
类、对象、方法、私有化
更新: 2025/2/24 字数: 0 字 时长: 0 分钟
类
类是将一个群体的共同特征(静态特征【属性】和动态特征【行为】)抽取出来之后得到的一个抽象概念。例如,我们前面学习的数据类型,其中整数类型就是纯数字,浮点类型就是带小数点的数字,每个群体都有自己所独有的一些特征。在程序的世界中,类是创建个体的“模具”,必须先定义了类,才能够基于类创建出属于该类的个体。在这个过程中,我们把创建个体的行为称之为“实例化”,把创建的个体称之为“实例对象”。
声明格式
当我们需要自定义类时,必须遵循以下声明格式:
class 类名:
"""说明文档"""
def 方法(参数1, 参数2...):
代码
class
:声明类的关键字。- 类名:类的标识符名称,大驼峰式命名(每个单词首字母大写)。
def
:声明函数的关键字。- 方法:声明在类中用于实现功能的函数。
创建对象
任何一种支持面向对象的编程语言,只要定义好了某个类,就可以基于该类创建无数个实例对象。在 Python 中创建对象的操作非常简单,只需要在类名后面加上 ()
括号,就会返回一个基于当前类所创建的新实例对象。同时,我们还可以用 isinstance
内置方法来判断对象是否属于某个类,代码如下:
class Per: # 声明一个Per类
pass
p1 = Per() # 注释:Per()返回一个Per类的实例对象,将对象引用赋值给了变量p1。
p2 = Per() # 注释:Per()返回一个Per类的实例对象,将对象引用赋值给了变量p2。
print(p1) # 输出:<__main__.Per object at 0x00000227CFE45370>
print(p2) # 输出:<__main__.Per object at 0x000001F146FF63A0>
print(isinstance(p1, Per)) # 输出:True。注释:变量p1所引用的对象属于Per类。
print(isinstance(p2, Per)) # 输出:True。注释:变量p2所引用的对象属于Per类。
print(p1 is p2) # 输出:False。注释:说明p1和p2引用的对象是两个不同的对象。
建议
在前面学习时我们说过“在 Python 中所有的数据类型都是对象”时,这意味着包括数字、字符串、函数、类等都是对象(变量除外)。这些对象具有一些共同的特征,例如它们都有类型,都可以被赋值给变量,都可以传递给函数等。而当我们说“类创建的对象”时,指的是通过类定义创建的实例对象,它们具有该类定义的属性和方法。
类变量
类会开辟新的作用域,所以定义在类中且在方法范围外的变量就属于类的局部变量,也叫做“类变量”。类变量不能在类的外部直接调用,必须通过 类名.类变量
或 对象.类变量
来调用,代码如下:
class Per:
# 定义类变量h
h = 61
print(h) # 输出:61
print(h) # 报错:变量'h'未定义。
print(Per.h) # 输出:61
print(Per().h) # 输出:61
重要
定义在类中且在方法范围外的代码,在程序运行的时候,会从上到下直接执行一次,往后不会再执行,即使创建对象也不会再执行。
类变量是整个类和对象所共享的公共属性,类和对象都可以直接对其进行赋值操作,代码案例如下:
class Per:
# 定义类变量n
n = ['Ti', 'To', 'Ta']
a = Per() # 注释:通过Per类创建对象,将对象引用赋值给变量a。
b = Per() # 注释:通过Per类创建对象,将对象引用赋值给变量b。
print(Per.n) # 输出:['Ti', 'To', 'Ta']
print(a.n) # 输出:['Ti', 'To', 'Ta']
print(b.n) # 输出:['Ti', 'To', 'Ta']
Per.n.remove('Ti') # 注释:通过类移除类变量n中的'Ti'元素。
print(Per.n) # 输出:['To', 'Ta']
print(a.n) # 输出:['To', 'Ta']
print(b.n) # 输出:['To', 'Ta']
a.n.remove('To') # 注释:通过对象a移除类变量n中的'To'元素。
print(Per.n) # 输出:['Ta']
print(a.n) # 输出:['Ta']
print(b.n) # 输出:['Ta']
b.n.remove('Ta') # 注释:通过对象b移除类变量n中的'Ta'元素。
print(Per.n) # 输出:[]
print(a.n) # 输出:[]
print(b.n) # 输出:[]
内置属性
当一个类被定义之后,可以通过 类名.内置属性
来获取类的相关信息,代码如下:
class Per:
"""人类""" # 注释:这里的注释就是类的说明文档。
h = 61 # 注释:定义一个类变量number赋值为61。
print(Per.__dict__) # 输出:{'__module__': '__main__', '__doc__': '人类', 'h': 61,...}。注释:__dict__内置属性返回了类级别属性和方法的字典(常用)。
print(Per.__name__) # 输出:Per。注释:__name__内置属性返回了类名的字符串。
print(Per.__module__) # 输出:__main__。注释:__module__内置属性返回当前类所在的模块名称。
print(Per.__doc__) # 输出:人类。注释:__doc__内置属性返回了类的说明文档。
print(Per.__bases__) # 输出:(<class 'object'>,)。注释:__bases__内置属性返回当前类的父类。
重要
值得记住的是,这里介绍的类的内置属性适用于 Python 中所有的类。有人可能会问类的这些内置属性是怎么来的呢?首先,我们要知道 Python 中所有的类都直接或间接地继承了 object
类。其次,在 object
类中有这些内置属性,所以继承它的类就可以使用这些内置属性,这也就是上面代码中 Per.__bases__
返回了 (<class 'object'>,)
即 object
类的原因。
对象
对象,简单讲就是拥有类的所有属性和所有功能的实体。在上面介绍类的时候,我们提到数据类型,其中整数类型就是纯数字,那么整数 4 就是整数类型的一个实例对象,而浮点类型就是带小数点的数字,那么小数 4.4 就是浮点类型的一个实例对象。
初始属性
前面提到,Python 中所有的类都直接或间接地继承自 object
类,在 object
类的源码中有一个专门初始化对象属性的 __init__
方法,源码如下(由于官方的解释器是用 C 语言写的,包括最内核、最核心的代码也是用 C 语言实现的,所以源代码显示的内容只有注释和一个 pass
关键词):
__init__
方法中形参带有默认参数self
代表当前对象,这个参数是不能省略的。__init__
方法不是创建对象的方法,而是初始化对象属性的方法。也就是说,在调用__init__
方法时,对象已经创建好了。__init__
方法不用我们手动调用,它会在对象创建后,由解释器自动调用来初始化对象属性。__init__
方法只在创建新对象后,初始化对象属性时执行一次,往后不会再执行。
class Per:
def __init__(self):
print(self)
Per() # 输出:<__main__.Per object at 0x000001C6A1BFFF60>。注释:对象创建好后,解释器会自动调用__init__方法来初始化对象属性。
p1 = Per() # 输出:<__main__.Per object at 0x000000F5BD6E3400>。注释:和上面输出self不同,说明Per()创建并返回一个新对象。
print(p1) # 输出:<__main__.Per object at 0x000000F5BD6E3400>。注释:和上面输出self一样,说明了self和变量p1引用的是同一个对象。
__init__
方法中初始化对象属性,其实就是对self.属性
进行赋值。
class Per:
def __init__(self): # 2.注释:自动调用__init__方法初始化对象属性。
self.name = 'Tim' # 3.注释:为对象初始化name属性并赋值为'Tim'。
p1 = Per() # 1.注释:Per类创建新对象。4.注释:将初始化后的对象赋值给p1变量。
print(p1.name) # 4.输出:Tim。注释:输出对象name属性。
__init__
方法和普通函数在参数位置、参数传递方面的规则是相同的,具体如下:- 参数赋值:无默认值的形参必须赋值,有默认值的形参可以不赋值,而且无默认值的形参必须放在有默认值的形参前面。
- 传参位置:可以按照字段的位置来传参,注意
self
形参不用传参,解释器会自动将当前对象传递给该参数。也可以按照对应的字段名称来传参,不过必须放在按字段位置传参之后,否则会报错。
class Per:
def __init__(self, name, age=18): # 注释:形参name无默认值必须赋值,形参age有默认值可以不赋值。
self.name = name # 注释:形参name赋值给对象的name属性。
self.age = age # 注释:形参age赋值给对象的age属性。
p1 = Per(name='Tim') # 注释:形参name赋值为'Tim'。
print(p1.name, p1.age) # 输出:Tim, 18。注释:输出对象name属性、age属性。
p2 = Per(name='Tom', age=20) # 注释:形参name赋值为'Tom',形参age赋值为20。
print(p2.name, p2.age) # 输出:Tom, 20。注释:输出对象name属性、age属性。
p3 = Per('Tam', 22) # 注释:形参name赋值为'Tam',形参age赋值为22。
print(p3.name, p3.age) # 输出:Tam, 22。注释:输出对象name属性、age属性。
操作属性
对象的属性是可以进行增删改查的操作,而且只会影响当前对象,不会影响新创建的对象。操作方法如下【注意,属性是属性,属性名是属性的字符串名称】:
查属性:通过对象的属性来输出对应值。
- 通过
对象.属性
来获取对象属性,如果属性不存在,会报错。 - 通过
对象.__getattribute__(属性名)
来获取对象属性,如果属性不存在,会报错。 - 通过
getattr(对象,属性名,默认值)
来获取对象属性,如果属性不存在,会返回默认值。
- 通过
增属性:通过给对象不存在的属性赋值来给对象添加新的属性。
- 通过
对象.属性 = 值
来给对象新属性赋值。 - 通过
对象.__setattr__(属性名,值)
来给对象新属性赋值。 - 通过
setattr(对象,属性名,值)
来给对象新属性赋值。
- 通过
改属性:通过给对象的已有属性进行赋值来修改对象的属性。
- 通过
对象.属性 = 值
来给对象属性赋值。 - 通过
对象.__setattr__(属性名,值)
来给对象属性赋值。 - 通过
setattr(对象,属性名,值)
来给对象属性赋值。
- 通过
删属性:删除对象已有的属性。
- 通过
del 对象.属性
删除对象已有的属性性,如果对象没有该属,会报错。 - 通过
对象.__delattr__(属性名)
删除对象已有的属性,如果对象没有该属,会报错。 - 通过
delattr(对象,属性名)
删除对象已有的属性,如果对象没有该属,会报错。
- 通过
class Per:
def __init__(self, name, age=18):
self.name = name
self.age = age
p = Per(name='Tim') # 注释:创建一个name为Tim,age为18的对象。
print(p.__dict__) # 输出:{'name': 'Tim', 'age': 18}。注释:以字典形式返回对象的所有属性和对应的值。
# 查属性
print(p.name) # 输出:Tim
print(p.__getattribute__('age')) # 输出:18
print(getattr(p, 'age')) # 输出:18
print(getattr(p, 'sex', 'None')) # 输出:None。注释:p对象没有sex属性,返回了'None'。
# 增属性
p.sex = 'man' # 注释:给p对象添加sex属性。
p.__setattr__('high', '175') # 注释:给p对象添加hight属性。
setattr(p, 'address', 'CN') # 注释:给p对象添加address属性。
print(p.sex, p.high, p.address) # 输出:man 175 CN
# 改属性
p.sex = 'woman' # 注释:修改p对象的sex属性。
p.__setattr__('high', '160') # 注释:修改p对象的hight属性。
setattr(p, 'address', 'US') # 注释:修改p对象的address属性。
print(p.sex, p.high, p.address) # 输出:woman 160 US
# 删属性
del p.sex # 注释:删除p对象的sex属性。
p.__delattr__('high') # 注释:删除p对象的hight属性。
delattr(p, 'address') # 注释:删除p对象的address属性。
print(getattr(p, 'sex', 'None')) # 输出:None
print(getattr(p, 'high', 'None')) # 输出:None
print(getattr(p, 'address', 'None')) # 输出:None
# 不影响新对象
q = Per(name='Tom') # 注释:创建一个name为Tom,age为18的对象。
p.sex = 'man' # 注释:给p对象添加sex属性。
print(q.sex) # 报错:sex属性只针对p对象添加,而新建的q对象中不会有sex属性。
对象属性的增删改查操作,是不是很像对某种数据类型的增删改查操作?对了,答案就是字典。在上面我们讲类的内置属性时,当中有一个 __dict__
内置属性,它用字典存储了类级别属性和方法。同样的,实例对象的属性管理也是依赖字典的,使用的也同样是 __dict__
内置属性,它里面存放了实例对象所有属性及对应值的字典,具体代码如下:
class Per:
def __init__(self, name, age, tel):
self.name = name
self.age = age
self.tel = tel
p = Per('Tim', 20, 123)
print(p.__dict__) # 输出:{'name': 'Tim', 'age': 20, 'tel': 123}
到这里我们就可以总结出一个结论了,针对实例对象属性的增删改查操作,实质就是对保存实例对象属性的字典进行增删改查。
class Per:
def __init__(self, name, age, tel):
self.name = name
self.age = age
self.tel = tel
p = Per('Tim', 20, 123)
print(p.__dict__) # 输出:{'name': 'Tim', 'age': 20, 'tel': 123}
p.__dict__['sex'] = 'woman'
print(p.sex) # 输出:woman
print(p.__dict__) # 输出:{'name': 'Tim', 'age': 20, 'tel': 123, 'sex': 'woman'}
p.__dict__['sex'] = 'man'
print(p.sex) # 输出:man
print(p.__dict__) # 输出:{'name': 'Tim', 'age': 20, 'tel': 123, 'sex': 'man'}
del p.__dict__['sex']
print(p.sex) # 报错:p对象没有sex属性。
print(p.__dict__) # 输出:{'name': 'Tim', 'age': 20, 'tel': 123}
约束属性
虽然我们可以给对象自由的添加新的属性,但在某些情况下我们希望对象所能添加的属性在指定的范围,这就是属性约束。在 Python 中进行属性约束的设置很简单,只需要将对象可用属性的字符串属性名放在一个可迭代对象(常用元组或列表)内,然后赋值给一个名称为 __slots__
的类变量即可。后续只要是通过这个类建立的对象,所能添加的属性只能是元祖当中限定的属性名,除元祖外的任何属性都不能被添加。
class Per:
# 约束类中的对象可以拥有的属性
__slots__ = ('name', 'age', 'sex')
def __init__(self, name, age, tel):
self.name = name
self.age = age
self.tel = tel # 报错:tel属性不在__slots__中,在初始化对象属性时就无法新增tel属性。
p = Per('Tim', 20, 123)
p.sex = 'man' # 注释:sex属性存在于__slots__中,因此可以新增sex属性。
print(p.sex) # 输出:man
p.high = 175 # 报错:high属性不存在于__slots__中,因此无法新增high属性。
需要注意的是,在类中定义了 __slots__
属性约束后,那么该类的实例对象就不能再使用上面我们讲的对象的 __dict__
属性了,具体代码如下:
class Per:
# 约束类中的对象可以拥有的属性
__slots__ = ('name', 'age', 'sex')
def __init__(self, name, age):
self.name = name
self.age = age
p = Per('Tim', 20)
print(p.__dict__) # 报错:'Per' object has no attribute '__dict__'。
方法
对象方法
对象方法,就是声明在类中且形参带有默认参数 self
的函数。特点如下:
- 对象方法中使用默认参数
self
来代表调用该方法的当前对象,因此在对象方法中可以通过self.类变量
来调用类变量,也可以通过self.对象属性
来调用对象的属性。 - 对象方法中第一个形参位置必须是默认参数
self
,但不用给它传递参数,解释器会自动将当前对象传递给该参数。 - 通过
对象.对象方法()
来调用对象方法。假如通过类.对象方法()
来调用对象方法,相当于执行普通函数,而且还缺少一个实例对象赋值给形参self
,因此不要通过类名来调用对象方法。
class Per:
# 形参中第一个参数是默认参数self,说明my_self是对象方法
def my_self(self):
print(self)
p1 = Per() # 注释:通过Per类创建对象赋值给变量p1。
print(p1) # 输出:<__main__.Per object at 0x000000F5BD6E3400>
p1.my_self() # 输出:<__main__.Per object at 0x000000F5BD6E3400>。注释:和上面输出变量p1结果一样的,说明了self就是当前对象p1。
Per().my_self() # 输出:<__main__.Per object at 0x000001C6A1BFFF60>。注释:和上面输出self不同,说明Per()创建并返回一个新对象。
重要
值得说明一下的是,“对象方法”与“函数”的区别在于,函数可以被独立执行,所有参数都以显式传递。而对象方法与一个 self
实例化对象关联,被隐式传递给调用它的对象,能够对类的实例中所包含的数据进行操作。
属性方法
除了对象方法外,还有一个属性方法,其特点如下:
- 使用装饰器
@property
修饰对象方法(带有@
就是装饰器)。 - 将对象方法转换为对象的只读属性,也就意味着该属性不能进行修改。
- 通过
对象.只读属性
调用。
class Person:
def __init__(self, f, l):
self.f = f
self.l = l
@property # 注释:带有@property装饰器,all不再是对象方法而是对象属性。
def all(self):
return f'{self.f}{self.l}'
p = Person("M", "C")
print(p.all()) # 报错,all不能被当作方法调用,只能用作对象属性。
print(p.all) # 输出:MC
p.all = "JC" # 报错:all是只读属性,不能被直接赋值修改。
p.f = "D" # 注释:可以通过修改对象的f属性来间接改变all属性。
print(p.all) # 输出:DC
类方法
前面我们讲了对象方法、属性方法,还有一个类方法,其特点如下:
- 使用装饰器
@classmethod
来装饰方法(带有@
就是装饰器)。 - 所有的类方法都有默认参数
cls
来代表当前的类,因此在类的方法中可以通过cls.类变量
来调用类变量。 - 默认参数
cls
不用传参,解释器会自动将当前类传递给默认参数。 - 通过
类.方法()
或对象.方法()
来调用类的方法。
class Per:
number = 61
@classmethod # 注释:带有@classmethod装饰器,因此hurt_earth是一个类方法。
def hurt_earth(cls): # 注释:cls指向当前类,即cls=Person。
print(cls.number) # 注释:因此cls.number等价于Person.number。
Per.hurt_earth() # 输出:61。注释:通过类名来调用类方法。
Per().hurt_earth() # 输出:61。注释:通过对象来调用类方法。
重要
类变量、方法都可以通过类或者该类创建的对象来调用,而对象的属性、方法只能通过对象调用。
静态方法
除了对象方法、属性方法、类方法,还有一个静态方法,其特点如下:
- 使用装饰器
@staticmethod
装饰方法(带有@
就是装饰器)。 - 没有默认参数。
- 通过
类.静态方法()
或对象.静态方法()
调用。
class Per:
@staticmethod # 注释:带有@staticmethod装饰器,因此protect_earth是一个静态方法。
def protect_earth():
print('地球')
Per.protect_earth() # 输出:地球。注释:通过类名调用静态方法。
Per().protect_earth() # 输出:地球。注释:通过对象调用静态方法。
重要
对象方法有默认参数 self
来绑定当前对象,因此在对象方法中可以通过 self
直接调用对象属性、类变量。类方法有默认参数 cls
来绑定当前类,因此在类方法中可以通过 cls
直接调用类变量。静态方法没有默认参数来绑定类和或象,因此在静态方法中必须通过类或对象来间接调用对象属性、类变量。
魔术方法
在 Python 的类中,以两个下划线 __
开头和结尾的方法通常都是有特殊用途和意义的方法,我们一般称之为“魔术方法”。例如,给对象属性进行初始化的 __init__
方法,就是一个魔术方法。除了该魔术方法以外,在 object
类中还有许多的魔术方法,挑几个重点的介绍:
__new__
方法:创建并返回对象。上面说过__init__
方法是初始化对象属性的方法,也就是说在对象属性初始化之前,对象就已经创建好了,而创建对象的方法就是__new__
方法。
class Person:
def __init__(self):
print('执行init', end='。')
def __new__(cls):
print('执行new', end=', ')
return object.__new__(cls) # 注释:object类的__new__方法会根据cls所绑定的类来创建对象。
Person()
'''
输出:执行new, 执行init。
注释:首先执行__new__方法,先输出了'执行new',然后执行object类的__new__方法创建对象,通过return返回了该对象。接着执行__init__方法,输出了'执行init'。如果__new__方法没有return新的对象,那么解释器也就不会调用__init__方法。
'''
建议
准确来讲,__new__
方法是一个魔法方法,它不属于静态方法或类方法的范畴。尽管 __new__
方法中有默认参数 cls
来代表当前的类,使得它在语法上看起来像一个类方法,但它并不需要用 @classmethod
装饰器来修饰,因为 Python 解释器会特殊处理这个魔术方法。
__repr__
方法:返回一个对象“正式”或“开发者”的字符串表示形式。常用于调试和开发过程中,以便程序员可以更容易地理解和跟踪对象的状态。使用print()
函数打印一个对象时,默认调用对象的__repr__
方法。
class Person:
def __init__(self, f, l):
self.f = f
self.l = l
def __repr__(self):
return f'{self.f}{self.l}'
person = Person("M", "C")
print(person) # 输出:MC。注释:默认调用__repr__方法。
print(repr(person)) # 输出:MC。注释:等价调用__repr__方法。
__str__
方法:返回一个对象“非正式”或“友好”的字符串表示形式。在__repr__
方法同时存在的情况下,使用print()
函数打印一个对象时,默认调用对象的__str__
方法。
class Person:
def __init__(self, f, l):
self.f = f
self.l = l
def __repr__(self):
return f'{self.f}{self.l}'
def __str__(self):
return f'{self.l}{self.f}'
person = Person("M", "C")
print(person) # 输出:CM。注释:默认调用__str__方法。
print(repr(person)) # 输出:MC。注释:等价调用__repr__方法。
print(str(person)) # 输出:CM。注释:等价调用__str__方法。
建议
总的来说,__str__
提供了一个友好的、易于阅读的字符串表示,而 __repr__
提供了一个更精确、更详细的表示,可能包含用于重建对象的所有必要信息。
__call__
方法:允许一个类的实例像函数一样被调用。具体来说,当定义了__call__
方法的类创建实例后,这个实例可以通过小括号()
进行“调用”,此时会自动执行__call__
方法中的代码,即对象.__call__()
等价于对象()
。
class Person:
def __init__(self, f, l):
self.f = f
self.l = l
def __call__(self):
return f'{self.f}{self.l}'
p = Person("M", "C")
print(p.__call__()) # 输出:MC。注释:通过对象p调用__call__对象方法。
print(p()) # 输出:MC。注释:因为Person类中有__call__方法,p是Person类的对象,因此p()等价p.__call__()。
__del__
方法:在对象即将被垃圾回收器回收之前被调用。主要用于执行一些清理操作,如关闭打开的文件、断开网络连接、释放系统资源等,以确保在对象销毁前不会留下任何未处理的资源。
class Res:
def __init__(self, filename):
self.file = open(filename, 'w', encoding='utf-8')
print(f"开始写入{filename}文件")
def __del__(self):
self.file.close()
print("文件已关闭,资源已释放。")
# 使用示例
res = Res("e.txt") # 输出:开始写入e.txt文件。
res.file.write("赛利亚") # 注释:写入内容。
# 当res对象不再被引用时,__del__方法将被调用
del res # 输出:文件已关闭,资源已释放。
警告
注意,由于 Python 的垃圾回收机制,__del__
方法的调用时间是不确定的,因此依赖 __del__
方法清理关键任务可能是不可靠的。另外,在 __del__
方法中执行的操作应尽可能简单且快速,避免执行复杂或耗时的任务。因此在大多数情况下,更推荐显式地管理资源,例如使用上下文管理器(with
语句)或提供显式的关闭/释放资源的方法,不是依赖 __del__
方法。
最后再扩展讲一下魔术方法,虽然看起来是第一次接触,但实际上我们开始学习 Python 的时候就已经接触到了,例如下面这个比较大小的代码:我们在 PyCharm 中按住 Ctrl 键点击 <
小于号进入源码,可以看到它实际对应 int
类中的 __lt__
魔术方法,通过注释可以明白该魔术方法的功能就是返回小于号比较后的结果。
print(1 < 2)
其实,在 Python 中每个比较运算符都对应一个魔术方法,常见的对应如下:只要类中定义了下列魔法方法,这些类的对象就可以相互比较大小,例如 int
类、float
类。而我们自定义的类中没有定义下列魔法方法,而且默认继承的 object
类中也没有定义下列魔法方法,因此通过自定义的类所创建的对象不能直接比较大小。
比较 | 符号 | 魔术方法 |
---|---|---|
大于 | > | __gt__ |
大于等于 | >= | __ge__ |
小于 | < | __lt__ |
小于等于 | <= | __le__ |
等于 | == | __eq__ |
不等于 | != | __ne__ |
私有化
在 Python 中约定以一个下划线(_
)开头的属性或方法名是“私有”的,即它们主要是供类的内部使用的,并不打算在类的外部被直接访问。需要注意的是,Python 的这种约定并不是强制性的,它更多是一种社区内广泛接受的编码风格。在实际开发中,遵守这些约定有助于保持代码的清晰性和可维护性。从技术上说,Python 并没有像 Java 或 C++ 那样的真正私有成员机制,因此这些以下划线开头的属性或方法仍然可以在类外部被访问和修改,代码如下:
class MyClass:
def __init__(self):
self._private_variable = 42
def _private_method(self):
return f"private variable is {self._private_variable}"
obj = MyClass()
# 虽然可以直接访问,但应谨慎使用。
print(obj._private_variable) # 输出:42
print(obj._private_method()) # 输出:private variable is 42
这里需要额外说明的是,在 Python 中以双下划线(_
)作为私有属性或私有方法名的开头,能阻止从类的外部直接访问,具体代码如下:
class MyClass:
def __init__(self):
self.__private_variable = 42
def __private_method(self):
return f"private variable is {self.__private_variable}"
obj = MyClass()
print(obj.__private_variable) # 报错:'MyClass' object has no attribute '__private_variable'
print(obj.__private_method()) # 报错:'MyClass' object has no attribute '__private_method'
在上面的例子中,我们尝试直接访问 __private_variable
和 __private_method
导致了 AttributeError
报错,会让人误以为 Python 实现了私有成员机制,这其实是 Python 给我们的一个错觉。究其原因是因为双下划线 _
开头的变量名和方法名会触发 Python 的名称重整(name mangling)机制,该机制会在变量名或方法名前加上类名(首字母大写)和一个下划线作为前缀,从而避免子类中的命名冲突,这也间接的导致了我们不能直接访问双下划线(_
)开头的变量名和方法名,但通过名称重整后的名称我们仍然可以访问到双下划线(_
)开头的变量和方法,代码如下:
class MyClass:
def __init__(self):
self.__private_variable = 42
def __private_method(self):
return f"private variable is {self.__private_variable}"
obj = MyClass()
print(obj._MyClass__private_variable) # 输出:42
print(obj._MyClass__private_method()) # 输出:private variable is 42
总的来说,使用单下划线(_
)开头的变量名和方法名则更多地被视为一种约定俗成的保护机制,用于表明这些成员是内部使用的,虽然它并没有像双下划线(_
)那样强的保护,但它是社区标准和最佳实践。因此在编写 Python 代码时,最好的做法仍然是使用单下划线(_
)来表示受保护的或内部的成员。最后要记住,Python 并没有强制实现真正的私有属性和方法,而是通过约定和名称重整来提供一种软性的私有性保护。同时,开发者应该尊重这些约定,并避免在类外部直接访问或修改这些被视为“私有”的属性或方法。