你好,我是hockor,今天咱们开始学习下 Python 中关于类的内容,我们会从最基础的类定义入手,逐步深入到属性、方法、继承、高级特性,并简要探讨两者的底层实现原理。
面向对象编程(Object-Oriented Programming, OOP)是一种强大的编程范式,它通过封装、继承和多态等核心概念,帮助开发者构建模块化、可维护和可扩展的软件系统。
从 JavaScript 的角度来看,class 关键字是ES6引入的语法糖,所以它的底层仍然基于原型继承,从本质上讲,JS的类仍然是函数,而类中定义的方法则被放置在构造函数的 prototype 对象上。
而Python 的类是类型(type)的实例,所有类(包括内置类)都是通过元类(默认是 type)动态创建的。类的定义会直接生成一个类对象。
python
class MyClass:
pass
print(type(MyClass)) # <class 'type'>,说明类是 `type` 的实例
print(isinstance(MyClass, type)) # True
python 类定义不是函数的语法糖,而是一个独立的语言结构,类体中的代码会在定义时执行,生成类属性和方法。
类定义初识
JS demo
JavaScript使用class关键字来定义一个类。类体内部可以包含构造函数(constructor)、实例方法、getter/setter、静态方法/字段以及私有字段。
class PersonJS {
constructor(name, age) {
this.name = name; // 实例属性
this.age = age;
console.log(`JS Person ${this.name} created.`);
}
introduce() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const john = new PersonJS("John", 30); // 调用构造函数
john.introduce();
class StudentJS extends PersonJS {
constructor(name, age, studentId) {
super(name, age); // 必须先调用 super 方法,然后再使用 this
this.studentId = studentId;
console.log(`JS Student ${this.name} (ID: ${this.studentId}) created.`);
}
}
const alice = new StudentJS("Alice", 20, "S12345");
alice.introduce();
JS中的构造函数constructor是JavaScript类中的一个特殊方法,用于创建和初始化通过new关键字创建的对象实例 。
-
一个类中只能有一个名为constructor的方法,否则会抛出SyntaxError。它不能是getter、setter、async或generator方法 。
-
当使用new操作符调用类时,constructor方法会自动执行。它主要用于设置实例的初始状态,通常通过this.propertyName = value的方式来定义实例属性 。
-
在派生类(使用extends关键字继承的类)中,如果定义了constructor,则必须在访问this关键字之前显式调用super()来调用父类构造函数 。这是JavaScript的严格要求,如果违反,将导致 ReferenceError。
-
如果未提供自定义构造函数,JavaScript会提供一个默认构造函数:基类(没有extends)的默认构造函数是空的constructor() {};派生类的默认构造函数则会调用super(...args) 。
Python demo
Python也使用class关键字来定义类,后跟类名和一个冒号。类体内部通常定义__init__方法(初始化器)、其他实例方法、类方法(@classmethod)、静态方法(@staticmethod)以及类属性(作为静态属性)。
# 定义一个名为 PersonPython 的基类
class PersonPython:
# 类的构造函数,初始化实例时自动调用
def __init__(self, name, age):
# 定义实例属性 name 和 age
self.name = name # 实例属性:姓名
self.age = age # 实例属性:年龄
# 打印创建信息
print(f"Python Person {self.name} created.")
# 定义一个实例方法
def introduce(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
# 创建 PersonPython 类的实例
jane = PersonPython("Jane", 25) # 调用 __init__ 方法,传入 name 和 age 参数
jane.introduce() # 调用实例方法 introduce
# 定义一个继承自 PersonPython 的子类 StudentPython
class StudentPython(PersonPython):
# 子类的构造函数,扩展了父类的功能
def __init__(self, name, age, student_id):
# 推荐做法:首先调用父类的构造函数
super().__init__(name, age) # 使用 super() 调用父类的 __init__ 方法
# 定义子类特有的实例属性 student_id
self.student_id = student_id
print(f"Python Student {self.name} (ID: {self.student_id}) created.")
# 创建 StudentPython 类的实例
alice_py = StudentPython("Alice", 20, "S54321") # 调用子类的 __init__ 方法
alice_py.introduce() # 调用继承自父类的 introduce 方法
Python中的初始化方法是__init__,__init__方法是Python中的一个特殊方法,通常被称为“构造器”或“初始化器” 。它的主要任务是在对象被创建后,初始化(赋值)类的数据成员。它在对象实例化时自动调用 。
-
__init__的第一个参数必须是self,它是一个约定俗成的名称,指向当前正在创建的对象实例 。通过 self.attribute_name = value来定义实例属性 。值得注意的是,在Python中,__new__方法才是真正负责创建对象实例的“构造函数”,而__init__仅仅是初始化对象状态的“初始化器” 。这种两阶段的对象创建过程是Python数据模型的一个独特之处 。 -
在继承中,子类的__init__方法通常会通过
super().__init__(*args, **kwargs)来调用父类的__init__方法,以确保父类中定义的属性得到正确初始化 。Python并不强制 super().__init__的调用顺序(不像JS),但为了避免属性未初始化的问题,强烈建议在子类__init__的开头调用父类构造函数 。也可以使用 ParentClass.__init__(self, *args, **kwargs)这种更显式的方式调用
对比表格
| 特性 | JavaScript (constructor) | Python (init) |
|---|---|---|
| 作用 | 创建并初始化对象实例 | 初始化对象实例的状态 |
| 关键字/方法名 | constructor | init (特殊方法/Dunder Method) |
| 自动调用 | 是(通过 new) | 是(通过对象实例化) |
| 第一个参数 | 无(this 隐式绑定) | self (显式绑定到实例) |
| 多构造函数 | 不允许(SyntaxError) | 不允许(但可通过工厂方法或默认参数模拟) |
| 派生类中调用父类 | 必须在访问 this 前调用 super() | 建议使用 super().init(),但调用顺序更灵活 |
| 返回值 | 基类可返回任意值(非对象被忽略),派生类必须返回对象或 undefined | 必须返回 None (隐式) |
高级特性
静态成员
JavaScript使用static关键字定义静态方法和静态字段。静态成员属于类本身,而不是类的实例。它们通常用于工具函数(如用于创建或克隆对象的功能)、缓存或固定配置数据,这些数据不需要在每个实例中复制 。静态成员通过类名直接访问,例如ClassName.staticMethod()或ClassName.staticField
class CalculatorJS {
// 静态属性 - 圆周率
static PI = 3.14159;
// 静态方法 - 两数相加
static add(a, b) {
return a + b;
}
}
// 调用静态属性
console.log(CalculatorJS.PI); // 输出: 3.14159
// 调用静态方法
console.log(CalculatorJS.add(5, 3)); // 输出: 8
// 注意:静态成员只能通过类访问
// const calc = new CalculatorJS();
// console.log(calc.PI); // 输出: undefined (实例无法访问静态成员)
Python对静态成员的处理方式与JavaScript有所不同:
-
静态方法 (@staticmethod): 使用@staticmethod装饰器定义。静态方法不接收隐式的第一个参数(self或cls),它们不访问或修改类或实例的状态 。它们是逻辑上属于类但独立于实例的工具函数,例如一个不依赖于任何特定对象状态的数学函数 。
-
静态属性(类属性): Python没有专门的@staticfield装饰器。其等价物是类属性,即定义在类体中、方法之外的变量 。这些类属性在所有实例之间共享,充当静态属性。所有实例都可以访问这些属性,并且对类属性的修改会影响所有实例。
class CalculatorPython:
# 类属性(相当于静态属性)
PI = 3.14159
# 静态方法 - 两数相加
@staticmethod
def add(a, b):
return a + b
# 访问类属性(静态属性)
print(CalculatorPython.PI) # 输出: 3.14159
# 调用静态方法
print(CalculatorPython.add(10, 20)) # 输出: 30
# 注意:虽然可以通过实例访问,但建议直接使用类名访问
# calc = CalculatorPython()
# print(calc.PI) # 可以访问但不推荐,应该使用 CalculatorPython.PI
Getter 与 Setter
JavaScript使用get和set关键字来定义getter和setter。它们允许开发者像访问普通属性一样访问方法,从而提供对属性的受控访问 。Getter方法用于获取属性值,而setter方法用于设置或修改属性值。Getter方法不接受参数,setter方法必须接受一个参数 。这些访问器属性被定义在类的原型prototype上
class TemperatureJS {
// 私有字段(存储实际温度值)
#celsius = 0;
// 构造函数
constructor(celsius) {
this.celsius = celsius; // 通过setter设置初始值
}
// celsius的getter方法
get celsius() {
console.log("获取摄氏温度...");
return this.#celsius;
}
// celsius的setter方法
set celsius(value) {
console.log("设置摄氏温度...");
if (value < -273.15) {
throw new Error("温度不能低于绝对零度(-273.15°C)");
}
this.#celsius = value;
}
// fahrenheit的getter方法(计算华氏温度)
get fahrenheit() {
return (this.#celsius * 9) / 5 + 32;
}
// fahrenheit的setter方法(转换为摄氏温度存储)
set fahrenheit(value) {
this.celsius = (value - 32) * 5 / 9; // 通过celsius的setter设置值
}
}
// 使用示例
const tempJS = new TemperatureJS(25); // 创建实例,初始温度25°C
console.log(tempJS.celsius); // 获取并打印摄氏温度
tempJS.fahrenheit = 68; // 设置华氏温度68°F(会自动转换为20°C)
console.log(tempJS.celsius); // 打印转换后的摄氏温度
// 错误示例(会抛出异常)
// tempJS.celsius = -300; // 尝试设置无效温度
在Python中实现getter和setter有多种方式,其中最“Pythonic”且推荐的方式是使用@property装饰器 。
-
传统方式: 可以通过定义独立的get_attribute()和set_attribute()方法来实现,但这不如@property优雅 。
-
@property装饰器: Python提供@property装饰器,它允许将方法转换为属性,从而以更简洁的方式实现getter和setter 。 @property用于标记getter方法,而 @<property_name>.setter则用于标记对应的setter方法 。这种方式使得外部代码可以像访问普通属性一样访问这些方法,同时在内部执行验证、计算或其他逻辑 。通常,内部存储的实际值会使用单下划线_前缀(例如_celsius),以遵循保护成员的约定。
class TemperaturePython:
def __init__(self, celsius=0):
"""
初始化温度实例
:param celsius: 初始摄氏温度,默认为0
"""
self._celsius = celsius # 受保护的内部变量(约定俗成)
@property
def celsius(self):
"""摄氏温度属性(获取)"""
print("正在获取摄氏温度...")
return self._celsius
@celsius.setter
def celsius(self, value):
"""摄氏温度属性(设置)"""
print("正在设置摄氏温度...")
if value < -273.15:
raise ValueError("温度不能低于绝对零度(-273.15°C)")
self._celsius = value
@property
def fahrenheit(self):
return (self.celsius * 9) / 5 + 32 # 通过摄氏温度getter计算
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5 / 9 # 通过摄氏温度setter设置
# 使用示例
if __name__ == "__main__":
temp_py = TemperaturePython(25) # 创建实例,初始温度25°C
print(temp_py.celsius) # 获取并打印摄氏温度
temp_py.fahrenheit = 68 # 设置华氏温度68°F(自动转换为20°C)
print(temp_py.celsius) # 打印转换后的摄氏温度
# 错误示例(会抛出异常)
# temp_py.celsius = -300 # 尝试设置无效温度
继承与多态
在 ES6 中,我们可以使用extends关键字来实现类的继承,子类可以继承父类的属性和方法 ,但是由于使用原型链继承(Prototypal Inheritance),所以在 JS 中是不支持多继承的,但是Python能支持多种继承方式,包括单继承、多重继承(一个类可以继承自多个父类)和多级继承(一个类继承自另一个已继承的类)。
我们先来看一个最简答的继承 demo
class AnimalPython:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a noise.")
class DogPython(AnimalPython): # 继承自 AnimalPython
def __init__(self, name, breed):
super().__init__(name) # 调用父类的初始化方法
self.breed = breed # 设置子类特有的breed属性
def bark(self):
print(f"{self.name} ({self.breed}) barks loudly!")
my_dog_py = DogPython("Max", "Labrador")
my_dog_py.speak() # 来自AnimalPython类的speak方法
my_dog_py.bark() # 来自DogPython类的bark方法
多继承的 demo
# 基类1 - 鸟类
class Bird:
def __init__(self, name):
self.name = name
def fly(self):
print(f"{self.name} 正在飞翔")
def speak(self):
print(f"{self.name} 发出鸟叫声")
# 基类2 - 马类
class Horse:
def __init__(self, name, speed):
self.name = name
self.speed = speed
def run(self):
print(f"{self.name} 以 {self.speed} km/h 的速度奔跑")
def speak(self):
print(f"{self.name} 发出马嘶声")
# 子类 - 飞马类 (同时继承Bird和Horse)
class Pegasus(Bird, Horse):
def __init__(self, name, speed):
# 由于两个父类都有name属性,我们只需要初始化一次
Horse.__init__(self, name, speed) # 显式调用Horse的初始化
Bird.__init__(self, name) # 显式调用Bird的初始化
def magical_ability(self):
print(f"{self.name} 展示了它的魔法能力!")
# 重写speak方法解决多继承的冲突
def speak(self):
print(f"{self.name} 发出神奇的鸣叫")
# 如果想调用特定父类的方法
Horse.speak(self) # 显式调用Horse的speak方法
# 创建飞马实例
pegasus = Pegasus("天马流星", 60)
# 调用从不同父类继承的方法
pegasus.fly() # 来自Bird类
pegasus.run() # 来自Horse类
# 调用子类自己的方法
pegasus.magical_ability()
# 调用被重写的方法
pegasus.speak()
# 查看方法解析顺序(MRO)
print(Pegasus.__mro__)
打印结果
天马流星 正在飞翔
天马流星 以 60 km/h 的速度奔跑
天马流星 展示了它的魔法能力!
天马流星 发出神奇的鸣叫
天马流星 发出马嘶声
(<class '__main__.Pegasus'>, <class '__main__.Bird'>, <class '__main__.Horse'>, <class 'object'>)
方法重写
JavaScript和Python都支持方法重写,其核心机制相似。Python提供了两种调用父类方法的方式super()和ClassName.method()。
# 定义基础形状类
class ShapePython:
def draw(self):
"""绘制基础形状"""
print("Drawing a generic shape.") # 打印绘制基础形状的信息
# 定义圆形类,继承自ShapePython
class CirclePython(ShapePython):
def draw(self):
"""绘制圆形(重写父类方法)"""
super().draw() # 推荐方式:调用父类的draw方法
# ShapePython.draw(self) # 替代方式:显式调用父类方法(不推荐)
print("Drawing a circle with specific details.") # 打印绘制圆形的详细信息
# 创建圆形实例
my_circle_py = CirclePython()
# 调用绘制方法
my_circle_py.draw()
底层机制简析
作为前端,我们都知道JavaScript是一种基于原型的语言。每个对象都有一个内部链接到另一个对象,称为其原型(prototype),这个原型对象也有自己的原型,如此往复,形成了原型链。
当访问对象的属性或方法时,JavaScript会首先在对象本身查找。如果找不到,它会沿着原型链向上查找,直到找到属性或到达链的末端(null)。
ES6的class关键字是原型继承的语法糖 。它只是提供了一种更清晰、更结构化的方式来定义构造函数、方法和继承,使其看起来更像传统的类继承模型。然而,它的底层机制仍然是原型链。例如,类中定义的方法实际上是添加到类的 prototype 属性上的,而实例则通过__proto__(或Object.getPrototypeOf())链接到这个原型对象 。
而Python的面向对象系统建立在其强大的数据模型之上,其中“一切皆对象” 。这意味着数字、字符串、函数,甚至类本身,都是对象。每个对象都具有身份、类型和值 。
Python的类行为由其数据模型和一系列特殊方法(通常以双下划线开头和结尾,如__init__, __str__, __len__等,这些方法允许类与内置操作(如len()函数、+运算符)进行交互,从而实现多态行为和自定义对象行为。
在Python中,类本身也是对象,它们是由**元类(metaclass)**创建的 。默认的元类是内置的type。当你定义一个类时,实际上是调用type()来创建这个类对象。元类可以被视为“创建类的类”,它们允许开发者在类被创建时修改其行为,例如自动添加方法、修改属性或强制执行编码标准 。元类是Python中非常高级且强大的概念,通常只在需要高度定制类创建行为时使用 。
JavaScript将原型性质隐藏在class语法背后,而Python则暴露了一个更明确的对象创建层次结构:对象是类的实例,而类又是元类(默认为type)的实例。这种层次结构是理解Python对象模型深度的关键。对于JavaScript开发者而言,这解释了Python对象模型背后的原理,以及为什么__init__和__new__等特殊方法会存在。
大白话来说就是:
JavaScript 的类:
-
表面看着简单,用 class 语法糖包装了复杂的原型继承,就像把发动机盖焊死了的车,你只知道怎么开,但看不到内部怎么运作
-
没有 Python 那种层层递进的创建关系
Python 的类:
-
把创建对象的"流水线"明明白白展示给你看:对象是类造出来的 → 类是元类(type)造出来的,就像乐高:积木(对象)←模具(类)←注塑机(元类)
-
能看到 new(造对象) 和 init(初始化对象) 两个步骤,还能自己改造"注塑机"(元类编程),造出特殊定制的类
ok,以上就是我们这一节的内容,我们下一节再见~