14  Python:语法解析-面向对象编程

“Python 是一门面向对象的编程语言”

14.1 简介

本章的目的不是教你编程,而是为了让你更好地理解 Python 的语法规则。

对于初学 Python 的人而言,“Python 是一门面向对象的编程 (OOP) 语言” 这样的表述想必并不陌生。然而,“面向对象”究竟是什么,尤其是“对象”的概念,却常常让人摸不着头脑。以众多 Stata 老用户为例,当他们初次见到下面这段代码时,往往会陷入困惑:

import statsmodels.api as sm
import pandas as pd

# 加载示例数据
mtcars = sm.datasets.get_rdataset('mtcars', 'datasets').data

# 模型 1
X1 = mtcars[['wt']]
X1 = sm.add_constant(X1)
y = mtcars['mpg']
model1 = sm.OLS(y, X1).fit()
print(model1.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    mpg   R-squared:                       0.753
Model:                            OLS   Adj. R-squared:                  0.745
Method:                 Least Squares   F-statistic:                     91.38
Date:                Sat, 24 May 2025   Prob (F-statistic):           1.29e-10
Time:                        01:31:42   Log-Likelihood:                -80.015
No. Observations:                  32   AIC:                             164.0
Df Residuals:                      30   BIC:                             167.0
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         37.2851      1.878     19.858      0.000      33.450      41.120
wt            -5.3445      0.559     -9.559      0.000      -6.486      -4.203
==============================================================================
Omnibus:                        2.988   Durbin-Watson:                   1.252
Prob(Omnibus):                  0.225   Jarque-Bera (JB):                2.399
Skew:                           0.668   Prob(JB):                        0.301
Kurtosis:                       2.877   Cond. No.                         12.7
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

mtcars = sm.datasets.get_rdataset('mtcars', 'datasets').data 这条语句中,呈现出 sm.A.B().C 的形式。初学者不免产生如下疑问:

  • 这里面哪些是函数 (function)?
  • 哪些是方法 (method)?
  • 哪些又是对象的属性呢?
  • 需要学习哪些概念才能搞清楚这些语句的编写规则?

同样令人费解的语句还有 model1 = sm.OLS(y, X1).fit()

上述问题其实是近两年 Python 学习者普遍面临的困惑。有了 AI 助手(如豆包、Copilot 等),很多人得以快速上手 Python。但好景不长,大家很快就会遭遇瓶颈。面对 AI 自动生成的代码,自己往往一知半解,基本不具备调试和修改代码的能力。这就导致在使用 Python 一段时间后,自身编写代码的能力始终难以显著提升,还总是花费大量时间进行调试,从而无法集中精力思考更高级的问题。

为此,本文将详细介绍 Python 面向对象编程的核心概念,帮你深入理解 Python 代码的逻辑和结构关系,进而提升编写 Python 代码的能力。接下来,我们就将逐步解答上述代码中的疑问。

14.2 类 v.s 对象:直观解释

解读 1 - 填写表格:空白表是「类」,填好的表式「对象 (实例)」

我们经常需要填写纸质或电子表格:去医院看病、网上购物、或者参加婚礼需要回复 RSVP。表格就是一种标准化的方式,方便他人或机构收集你的信息。不同的表格关注的信息各不相同:你会在医生的表格上填写敏感的健康状况,而婚礼邀请回复表格则关注你会带几位嘉宾 —— 这两种信息互不相干。

在 Python 中,class(类)、type(类型)、data type(数据类型)这些术语基本等价。可以按照如下逻辑来理解:

  • 可以把看作是 Python 对象(也称为「实例」)的模板 (blueprint)
  • 对象则是具体的数据实体,代表某个「名词」(可以是医院里的病人、网上的购物订单、婚礼的嘉宾等等)。
  • 就像是一张空白表格模板,而对象就是基于这个模板,实际填写了数据的具体表格。

比如,下图中的 RSVP 回复表格就是类的一个例子,而具体填写好的 RSVP 回执,就是一个对象:

Source:15 Object-Oriented Programming and Classes

另一种类比:电子表格(Excel)

你还可以把类和对象类比为电子表格(比如 Excel),如下图所示。表格的列名就类似于类(定义了每个属性),而表格的每一就相当于一个具体对象(包含具体的数据)。

下面,我们做详细介绍。

14.3 类、对象、封装、继承和多态

14.3.1 类(Class)

类是对象的模板,它定义了对象的属性和方法,可将其视为一种自定义的数据类型。以 Person 类为例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        print(f"我叫 {self.name},今年 {self.age} 岁。")
        

Person 类包含两个函数,分别是 __init__()introduce()。前者用于定义「对象的属性」,后者用于定义「对象的方法」。具体说明如下:

  • __init__() 是一个特殊的函数,称为「构造函数」。它的作用是在创建类的对象时,初始化 对象的属性。在这个例子里,__init__() 定义了 Person 类的两个属性,即 nameage
  • introduce() 函数定义了 Person 类的一个方法,其功能是打印出对象的 nameage 信息。

这里要着重说明一下 self 这个参数。在 Python 的类中,self 是一个约定俗成的参数名,它代表类的实例对象本身。当你调用类的方法时,Python 会自动将调用该方法的对象作为第一个参数传递给 self。借助 self,方法能够访问和修改对象的属性。

例如,在 __init__() 方法里,self.name = name 这一操作把传入的 name 参数值赋给了对象的 name 属性;在 introduce() 方法中,self.nameself.age 用于获取对象的 nameage 属性值。

14.3.2 对象(Object)

对象是类的实例。通过类能够创建多个不同的对象,每个对象都有自己独立的属性值。例如:

p1 = Person("张三", 20)
p2 = Person("李四", 25)

print(p1.name)  # 输出: 张三
print(p2.name)  # 输出: 李四

p1.introduce()  # 输出: 我叫 张三,今年 20 岁。
p2.introduce()  # 输出: 我叫 李四,今年 25 岁。
张三
李四
我叫 张三,今年 20 岁。
我叫 李四,今年 25 岁。

此例中,p1p2Person 类的两个不同对象。p1.name 的值是 “张三”,p2.name 的值是 “李四”,这体现了不同对象的属性可以有不同的值。

14.3.3 封装(Encapsulation)

封装是面向对象编程的一个重要特性,它指的是将数据(属性)和操作数据的方法捆绑在一起,并且对外部隐藏对象的内部实现细节。封装的作用主要体现在以下几个方面:

  • 数据保护:防止外部代码直接访问和修改对象的属性,从而避免数据被意外修改或破坏。
  • 简化接口:只向外部暴露必要的方法,隐藏内部实现细节,降低外部代码与对象之间的耦合度,使代码更易于维护和扩展。

在 Python 中,可以通过访问控制来实现封装。虽然 Python 没有像其他语言那样严格的访问修饰符(如 privateprotected),但可以通过约定来表示属性或方法的访问级别。以单下划线开头的属性或方法被视为受保护的,以双下划线开头的属性或方法被视为私有的。例如:

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance

    def deposit(self, amount):
        self.__balance += amount

    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("余额不足")

    def get_balance(self):
        return self.__balance

account = BankAccount(1000)
account.deposit(500)
account.withdraw(200)
print(account.get_balance())  # 输出: 1300
1300

在这个例子中,__balance 是一个私有属性,外部不能直接访问,只能通过 depositwithdrawget_balance 等方法来操作。这样就保证了账户余额的安全性,外部代码无法随意修改余额。

14.3.4 4. 继承(Inheritance)

继承是指一个类可以继承另一个类的属性和方法。被继承的类称为父类(基类),继承的类称为子类(派生类)。子类可以扩展父类的功能,也可以重写父类的方法。例如:

class Student(Person):
    def __init__(self, name, age, student_id):
        super().__init__(name, age)
        self.student_id = student_id

    def study(self):
        print(f"{self.name} 正在学习。")

stu = Student("王五", 18, "001")
stu.introduce() 
stu.study()    
我叫 王五,今年 18 岁。
王五 正在学习。

在这个例子中,Student 类继承了 Person 类的属性和方法,并添加了自己的属性 student_id 和方法 studysuper().__init__(name, age) 调用了父类的构造函数,对 nameage 属性进行初始化。

14.3.5 多态(Polymorphism)

多态是指不同的对象可以对同一个操作做出不同的响应。下面通过几个例子来帮助你理解多态的概念和应用。

14.3.5.1 简单数据类型示例

a = 5
b = "apple"
print(a * 3)  # 输出: 15
print(b * 3)  # 输出: appleappleapple

可见,* 运算符对于整数 a 和字符串 b 有不同的行为:

  • 对于整数,* 表示乘法运算;
  • 对于字符串,* 表示重复字符串。

这就是多态的体现,同一个运算符在不同的数据类型上有不同的操作。

14.3.5.2 类的方法重写示例


class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius * self.radius

shapes = [Rectangle(4, 5), Circle(3)]
for shape in shapes:
    print(shape.area())
20
28.259999999999998

在这个例子中,RectangleCircle 类都继承自 Shape 类,并且重写了 area 方法。当我们遍历 shapes 列表并调用每个对象的 area 方法时,不同的对象会根据自己的实现方式计算并返回面积,这也是多态的应用。

14.3.5.3 Python 支持多态的原因

Python 能够支持多态,主要得益于其动态类型的特性。在 Python 中,变量在声明时不需要预先指定数据类型,变量的类型是在运行时动态确定的。这意味着一个变量可以在不同的时刻引用不同类型的对象。当调用一个函数或方法时,Python 不会关心对象的具体类型,只要对象实现了所需的方法或属性,就可以正常调用。

这种特性使得 Python 代码更加灵活,能够轻松实现多态。例如,在上面的 shapes 列表中,RectangleCircle 对象虽然类型不同,但都实现了 area 方法,因此可以统一调用 area 方法来计算面积。

14.3.6 面向对象概念在具体包和函数中的应用

在实际的 Python 编程中,面向对象编程的概念广泛应用于各种包和函数中。下面以 pandasmatplotlib 这两个常用的包为例进行说明。

14.3.6.1 pandas 中的应用

pandas 是一个用于数据处理和分析的强大包,其中大量使用了类和对象的概念。例如,DataFramepandas 中最常用的类之一,用于表示二维表格数据。

import pandas as pd

# 创建一个 DataFrame 对象
data = {
    'Name': ['张三', '李四', '王五'],
    'Age': [20, 25, 18]
}
df = pd.DataFrame(data)

# 使用 DataFrame 的方法
print(df.head())      # 查看数据集行数
print('-' * 20)
print(df.describe().T.round(2))  # T 表示转置
  Name  Age
0   张三   20
1   李四   25
2   王五   18
--------------------
     count  mean   std   min   25%   50%   75%   max
Age    3.0  21.0  3.61  18.0  19.0  20.0  22.5  25.0

此例中,pd.DataFrame 是一个类,dfDataFrame 类的一个对象。

我们可以调用 df 对象的各种方法,如 head()describe(),来对数据进行操作和分析。甚至可以采用链式调用的方式来连续执行多个操作,如 df.describe().T.round(2) 表示对 df 对象依次进行如下操作:计算基本统计量 → 转置 → 四舍五入 (保留两位有效数字)。

14.3.6.2 matplotlib 中的应用

matplotlib 是一个用于数据可视化的包,也广泛应用了面向对象编程的思想。例如,我们可以使用 FigureAxes 类来创建和定制图形。

import matplotlib.pyplot as plt

# 创建一个 Figure 对象和一个 Axes 对象
fig, ax = plt.subplots(figsize=(2, 2))

# 绘制数据
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]
ax.plot(x, y)

# 设置图形属性
ax.set_title('line plot')
ax.set_xlabel('X')
ax.set_ylabel('Y')

# 显示图形
plt.show()

在这个例子中,figFigure 类的对象,axAxes 类的对象。

我们可以通过调用 ax 对象的方法,如 plot()set_title()set_xlabel()set_ylabel() 来绘制和定制图形。这体现了面向对象编程在 matplotlib 中的应用,通过将图形的不同部分封装成对象,使得图形的创建和定制更加灵活和可控。

14.4 包、函数、方法和属性

我们先简要介绍一下理解复杂语句所需的基础概念。

14.4.1 包(Package)和模块(Module)

14.4.1.1 模块(Module)

  • 模块是单个 .py 文件,包含函数、类、变量等代码。
  • 例如:
    • arrow.api.py 是一个模块,包含 get() 函数。
    • numpy.random.py 也是一个模块,内含若干个用于生成随机数的函数。

14.4.1.2 包(Package)

包是一个包含多个模块的目录,通常还包含一个 __init__.py 文件。

一个包中可以包含多个子包,而每个包中又可以包含多个模块。包的目录结构类似于文件系统的目录结构。__init__.py 文件的作用是标识该目录是一个包,更重要的是,它详细记录了这个包里都有哪些模块和子包。

包通过目录结构组织代码,例如:

pandas/               # 根包
  __init__.py         # 标识这是一个包
  core/               # 子包
    __init__.py
    frame.py          # 模块(定义 DataFrame)
    series.py         # 模块(定义 Series)
  io/                 # 子包
    __init__.py
    excel.py          # 模块(处理 Excel 文件)

比如,arrow 是一个用于处理日期和时间的包,该包的程序文件存放于 arrow 目录下:

其中包含 __init__.py 文件和其他模块文件(如 arrow.pyutil.py 等)。当你使用 import arrow 时,Python 会自动执行 __init__.py 文件中的代码,从而初始化包的命名空间。

你若执行 from arrow import api,则会导入 arrow 包中的 api 模块。然后你可以使用 api 模块中的函数和类,例如 api.get()

import arrow as ar

# 查看 arrow 的类型
print(type(ar)) 

# 查看 arrow 的路径
print(ar.__file__)  

# 输出类似:/path/to/site-packages/arrow/__init__.py
# 这表明 arrow 是包含多个子包的根目录。
<class 'module'>
c:\ProgramData\anaconda3\Lib\site-packages\arrow\__init__.py

同理,你可以使用如下方法查看其他包的路径:

import pandas as pd
print(pd.__file__)  # 查看 pandas 的路径
c:\ProgramData\anaconda3\Lib\site-packages\pandas\__init__.py

14.4.2 函数(Function)和方法(Method)

函数是一段完成特定任务的独立代码块,可以直接调用。而方法是与对象关联的函数,必须通过对象来调用。例如,

  • sm.datasets.get_rdataset() 中,get_rdataset() 是一个函数,它属于 statsmodels 包中 datasets 模块。
  • model1 = sm.OLS(y, X1).fit() 中:
    • OLS()sm 包中的一个函数,用于创建一个普通最小二乘回归模型对象。
    • sm.OLS(y, X1) 创建了一个 OLS 对象
      • fit()sm.OLS(y, X1) 创建的对象的方法

14.4.2.1 类中的属性(Attribute)和方法(Method)

属性 是类或对象的数据成员,而 方法 则是类或对象的函数成员。

因此,在 mtcars = sm.datasets.get_rdataset('mtcars', 'datasets').data 中,.data 就是 get_rdataset() 函数返回的对象的一个属性,里面存储着对象里的数据。

14.5 解析复杂语句

现在我们来详细解析引言中提到的复杂语句:

import statsmodels.api as sm

# 加载示例数据
mtcars = sm.datasets.get_rdataset('mtcars', 'datasets').data
mtcars.head()

具体解释如下:

  • smstatsmodels.api 模块的别名。
  • datasetssm 下的一个子模块,专门用于访问内置或外部数据集。
  • get_rdataset('mtcars', 'datasets')datasets 子模块中的一个函数,用于从 R 的数据集仓库中的 datasets 包中获取名为 'mtcars' 的数据集。这个函数会返回一个包含数据集及其元信息 (meta information) 的对象 (可以假想为名称为 mtcars_object 的对象)。
  • .data 是对象 mtcars_object 的一个属性,用于提取实际的数据内容(通常为一个 pandas.DataFrame)。
  • 因此,最终得到的 mtcars 变量就是一个 pandas.DataFrame 对象,包含了 'mtcars' 数据集的实际数据。
  • mtcars.head()pandas.DataFrame 对象的方法,用于查看数据集的前几行数据。

需要注意的是,在上述语句中,datasets 这个关键词出现了两次,两者含义不同,不要混淆:

  • 第一次是指 sm 下的 datasets 子模块;
  • 第二次是作为参数传递给 get_rdataset() 函数,它是 R 语言中的一个扩展包的名称,而 mtcars 则是 datasets 包中的一个数据集。

把上述代码拆成两行更有助于理解 模块函数属性 这三个概念的区别:

import statsmodels.api as sm

# 获取数据集对象:'object = 模块.函数()' 格式
#        本例中:'object = 模块.子模块.函数(参数1, 参数2)' 格式
mtcars_object = sm.datasets.get_rdataset('mtcars', 'datasets') 

# 获取数据集内容:'object.属性' 格式
mtcars_df = mtcars_object.data 

可以将这行代码拆解为以下几步,更便于初学者理解:

# 第一步:获取 datasets 子模块
datasets_module = sm.datasets

# 第二步:调用 get_rdataset 函数获取数据集对象
rdataset = datasets_module.get_rdataset('mtcars', 'datasets')
  # - 'mtcars' 是数据集的名称
  # - 'datasets' 是数据集所在的包名
# 第三步:通过 data 属性获取实际的数据
mtcars = rdataset.data

至此,代码的第二部分也很容易理解了:

# 模型 1
X1 = data[['weight']]
X1 = sm.add_constant(X1)
y = data['mpg']
model1 = sm.OLS(y, X1).fit()

解释如下:

  • sm.OLS(y, X1)
    • OLSstatsmodels.api 模块中的一个类,用于创建一个普通最小二乘回归模型的对象。这里传入的参数 y 是因变量,X1 是自变量。
  • .fit():是 sm.OLS(y, X1) 创建的对象的方法,用于拟合模型,即根据输入的数据计算模型的参数。

14.6 参考资料

14.7 总结

  • 模块是单个 .py 文件,包含函数、类、变量等代码。
  • 包是一个包含多个模块的目录,通常还包含一个 __init__.py 文件。
  • 类是对象的模板,它定义了对象的属性和方法。
  • 对象是类的实例。通过类能够创建多个不同的对象,每个对象都有自己独立的属性值。
  • 封装是将数据(属性)和操作数据的方法捆绑在一起,并对外部隐藏对象的内部实现细节。
  • 继承是一个类可以继承另一个类的属性和方法。
  • 多态是指不同的对象可以对同一个操作做出不同的响应。
  • 函数是一段完成特定任务的独立代码块,可以直接调用。