Python3 基础教程

Python3 面向对象编程

🎉摘要:本章深入讲解 Python3 面向对象编程核心知识。内容涵盖面向对象与面向过程对比、类与对象定义、__init__ 构造方法、实例方法与属性、访问限制、@property 装饰器以及继承与方法重写。通过丰富代码示例,帮助开发者掌握 Python OOP 编程技巧,提高代码复用性与扩展性。

在前面章节已经详细介绍了 Python3 的基础知识,你可以利用这些基础知识编写一些小功能(对字符串进行裁剪、简单数学运算等等),甚至可以操作本地的文件(读取和写入),但 Python3 的功能远远不止这些,它还支持面向对象编程,本章将介绍 Python3 关于面向对象编程相关的知识。

什么是面向对象?

谈到面向对象编程,不得不了解一下面向过程,然后通过对比两者,深刻认识面向对象的好处。

面向过程(即结构化)

编程指以过程或函数为核心来组织程序的逻辑结构。数据在流程中流动,自顶向下、分层模块化,顺序执行。

在面向过程编程中,整个程序被设计成一系列顺序执行的步骤,通过将复杂的任务分解为较小的、相互关联的子任务或函数来实现程序功能。这些函数按照一定的顺序依次调用,以完成特定的业务逻辑。如下图:

上图从主函数开始运行,一步一步的执行,直到程序结束。

面向过程强调数据的处理流程,从输入数据开始,经过一系列预定的处理步骤,最终产生输出结果。

面向过程编程注重程序执行的顺序和步骤的清晰性,有利于提高代码的可读性和可维护性,尤其适用于处理较为简单和线性的问题场景。

例如,在开发一个简单的文件处理程序时,可以使用面向过程编程,将打开文件、读取数据、处理数据、写入结果到文件等操作分别定义为不同的函数,然后按照流程依次调用这些函数来完成整个文件处理任务。如下:

面向对象

面向对象是指将数据和对数据的操作封装在一起,形成一个个独立的“对象”。

面向对象编程(Object-Oriented Programming, OOP)是以对象为核心的编程范式,通过将现实世界中的事物抽象为类(Class)对象(Object),来解决复杂问题。OOP 的核心思想是通过对象的交互来实现功能,而不是依赖函数的顺序执行。

例如,以洗衣机洗衣服为例,我们只和洗衣机对象进行交互,不用管对象内部怎么干。如下图:

通过面向对象编程,开发者能够更高效地组织代码,提高代码的复用性和扩展性,便于应对日益复杂的软件开发需求。

面向对象编程(OOP)主要特性包括:

  • 封装:将数据和方法封装在一个类中,隐藏实现细节,只暴露必要的接口供外部访问。
  • 继承:允许新类从现有类中继承属性和方法,实现代码重用。
  • 多态:不同对象可以调用相同的方法,产生不同的执行结果,增加代码的灵活性。
  • 抽象:通过抽象类和接口来定义对象的特征和行为,使得代码更加清晰和易于维护。

注意,这些特性使得面向对象编程能够更直观地表达问题和解决方案,提高代码的可重用性、可扩展性和可维护性。

什么是类与对象?

类(Class)和对象(Object)是面向对象编程的两大核心。一个类可以实例化多个对象。可以简单理解为:类是产品的设计图纸,对象则是依据图纸生产出的具体实例。

类(Class)

类(Class)是对一类事物的抽象描述与模板。类不代表某个具体东西,而是定义了这类东西共同拥有什么特征、能做什么事。例如:

  • 人不是某一个人,而是“所有人”的统称。
  • 手机不是某一台手机,而是“所有手机”的模板。

在面向对象的世界里,类 = 模板 / 图纸 / 设计方案。

注意,一个标准的类,主要包含如下两部分:

  • 属性(特征):用来描述某类事物“长什么样、有什么信息”。比如人类,拥有姓名、年龄、身高、性别等属性信息。
  • 方法(行为 / 能力):用来描述某类事物“能做什么事”。比如人类,能够吃饭、睡觉、走路、工作等能力。

对象(Object)

对象(Object)是类的具体实例,是真实存在、可以使用的个体。

类是抽象模板,对象就是根据这个模板造出来的真实东西

类是图纸,对象就是按图纸造好的成品

类是“手机”这个概念,对象就是你手里正在用的这部手机。

总结起来:

  • 对象 = 类的具体实现 = 真实可操作的实体
  • 类是模板,对象是实例
  • 类定义结构,对象存具体数据

类的定义和使用

在 Python 中,类表示具有相同属性和方法的对象的集合。在使用类时,需要先定义类,然后创建类的实例,通过类的实例就可以访问类中的属性和方法。下面将进行具体介绍:

定义类

在 Python 中,类的定义使用 class 关键字来实现,语法如下:

class 类名称:
    """类的帮助信息"""		# 类文档字符串
    statement				# 类的代码体

参数说明如下:

  • class:是 Python 关键字,用来定义类,告诉解释器“我要开始定义一个类了”
  • 类名称:你自己给类起的名字,命名规则:大驼峰命名法(每个单词首字母大写),例如:
    • 正确:Person、Student、Car、UserInfo
    • 错误:person、student、user_info
    • 注意,不能使用 Python 的关键字(如 if/for/class 等)
  • : 冒号,语法符号,表示“类的内容开始了”
  • """类的帮助信息""":用三引号包裹的注释,用作说明这个类是干什么用的,可以通过 help(类名) 查看,方便别人(包括未来的你)看懂代码。
  • statement:指类里面真正的代码,必须缩进(4 个空格或 1 个 Tab),注意,类代码体主要包含如下信息:
    • 类属性:所有实例共享的变量
    • __init__():构造方法(初始化属性)
    • 实例方法:对象的行为
    • 类方法@classmethod
    • 静态方法@staticmethod
    • 私有属性 / 方法(__xxx)

注意,在定义类时,如果没想好类的具体功能,可以在类体中直接使用 pass 语句代替。

示例:定义一个名为 Person 的类,该类拥有一个构造方法和成员方法,将在后续详细介绍

class Person:
    # 构造方法:创建实例自动执行
    def __init__(self, name, age):
        # self = 当前创建的对象自己
        self.name = name   # 实例属性
        self.age = age     # 实例属性

    # 成员方法
    def introduce(self):
        print(f"我叫{self.name},今年{self.age}岁")

创建类的实例

定义完类后,并不会真正创建一个实例。这就有点像一辆汽车的设计图,设计图可以告诉我们汽车是什么样子,如何进行制造。设计图并不是一辆汽车,也不能被开走,它只能用来制造真正的汽车,而且可以制造很多汽车。

在 Python 中,创建类的实例语法如下:

ClassName(parameterList)

其中:

  • ClassName 必填,用来指定具体的类名,如 Person。
  • parameterList 可选,用来为实例指定构造参数,如果类没有创建 __init__() 方法,或者 __init__() 方法只有一个 self 参数,则 parameterList 可以被省略。关于 __init__() 将在后面介绍。

示例 1:一个非常简单的类,打印问候信息

class Hello:
    """第一个类"""
    # 打招呼的方法
    def hi(self, name):
        print(f"你好!{name}")

# 创建 Hello 类的实例
if __name__ == "__main__":
    p = Hello()
    p.hi("张三")

运行结果:

你好!张三

示例 2:创建一个拥有构造参数的 Person 类,通过构造参数传递值

class Person:
    # 构造方法:创建实例自动执行
    def __init__(self, name, age):
        # self = 当前创建的对象自己
        self.name = name   # 实例属性
        self.age = age     # 实例属性

    # 成员方法
    def introduce(self):
        print(f"我叫{self.name},今年{self.age}岁")

if __name__ == '__main__':
    p1 = Person("张三", 20)
    p1.introduce()
    p2 = Person("李四", 30)
    p2.introduce()

运行结果:

我叫张三,今年20岁
我叫李四,今年30岁

注意,__name__ == '__main__' 是 Python 里用来判断当前脚本是不是被直接运行的常用写法。当你直接运行这个 .py 文件时,Python 会自动把内置变量 __name__ 设为 '__main__',下面的代码就会执行。当这个文件被别的脚本 import 导入时,__name__ 会变成模块名,条件不成立,下面的代码就不会运行。

创建 _init__() 方法

在创建类后,通常会创建一个 __init__() 方法,该方法是一个特殊的方法,类似 Java 语言中的构造方法。每当创建一个类的新实例时,Python 都会自动执行它。

注意,__self__() 方法中必须包含一个 self 参数,并且必须是第一个参数。self 参数是一个指向实例本身的引用,用于访问类中的属性和方法。在方法被调用时会自动传递实际参数 self。因此,在 __init__() 方法中只有一个参数的情况下,不需要指定实际参数。

示例 1:仅有 self 参数的构造方法

class Hello:
    """打招呼的类"""
    # 构造函数
    def __init__(self):
        print(f"Hello 类的初始化方法")

    # 打招呼的方法
    def hi(self, name):
        print(f"你好!{name}")

# 创建 Hello 类的实例
if __name__ == "__main__":
    p = Hello()
    p.hi("张三")

执行结果:

Hello 类的初始化方法
你好!张三

从运行结果可以看出,在创建 Hello 类的实例时,虽然没有为 __init__() 方法指定参数,但是该方法会自动执行。

示例 2:定义一个构造方法,接收用户姓名,当调用 hi() 方法时自动输出打招呼信息

class Hello:
    """打招呼的类"""
    # 定义成员
    name = "" # 姓名

    # 构造函数
    def __init__(self, name):
        self.name = name

    # 打招呼的方法
    def hi(self):
        print(f"你好!{self.name}")

# 创建 Hello 类的实例
if __name__ == "__main__":
    p = Hello("张三")
    p.hi()

运行结果:

你好!张三

示例 3:使用构造函数接收两个业务参数,分别是用户的姓名和年龄,其中年龄设置默认值

class Person:
    # 构造函数,并且为 age 指定默认值 18
    def __init__(self, name, age=18):
        self.name = name
        self.age = age

if __name__ == "__main__":
    p1 = Person("小红")
    print(p1.name) # 小红
    print(p1.age)  # 18

运行结果:

小红
18

创建类的成员并访问

类的成员主要由实例方法和数据成员组成。在类中创建了类的成员后,可以通过类的实例进行访问。

创建实例方法并访问

所谓的实例方法,是指在类中定义的函数。该函数是一种在类的实例上操作的函数。同 __init__() 方法一样,实例方法的第一个参数必须是 self,并且必须包含一个 self 参数。

创建语法如下:

def functionName(self, parameterlist):
    block  # 函数体代码块

参数说明:

  • functionName:用于指定方法名称,一般使用小写字母开头。
  • self:必要参数,表示类的实例,其名称可以是 self 以外的单词,使用 self 只是一个习惯。
  • parameterlist:用于指定除了 self 参数以外的参数,各个参数使用逗号“,”进行分割。
  • block:方法体,一个代码块,是方法的具体功能实现。

在成功创建实例方法后,可以通过类的实例名称和点(.)操作符进行访问,语法如下:

instanceName.functionName(parameterList)

参数说明:

  • instanceName:为类的实例名称
  • functionName:为要调用的方法名称。
  • parameterList:表示为方法指定的实际参数,值的个数与创建实例方法时parameterList 个数一致。

示例:

class Person:
    # 构造函数,并且为 age 指定默认值 18
    def __init__(self, name, age=18):
        self.name = name
        self.age = age
        self.deposit = 0
    
    # 发工资
    def payoff(self, salary):
        self.deposit += salary
    
    # 打印对象的方法
    def print_info(self):
        print(f"姓名:{self.name},年龄:{self.age}, 存款:{self.deposit:.2f}")

if __name__ == "__main__":
    p1 = Person("小红", 20)
    p1.payoff(1000) # 发工资 1000
    p1.print_info()

运行结果:

姓名:小红,年龄:20, 存款:1000.00

创建数据成员并访问

数据成员指在类中定义的变量,即属性,根据定义位置,又可以分为类属性和实例属性。下面将分别介绍:

  • 类属性

类属性是指定义在类中,并且在函数体外的属性。类属性可以在类的所有实例之间共享值,也就是在所有实例化的对象中公用。注意,类属性可以通过类名称或者实例名被访问。

示例 1:在 Person 类中定义两个类属性,分别表示姓名和年龄。

class Person:
    # 类属性
    name = "小红"
    age = 20

    # 构造函数,内部将访问类属性
    def __init__(self):
        print("Person 构造函数")
        print(f"姓名:{self.name},年龄:{self.age}")

if __name__ == "__main__":
    Person() # 调用构造函数
    # 使用类名称访问类属性
    print(f"姓名:{Person.name}, 年龄:{Person.age}")

运行结果:

Person 构造函数
姓名:小红,年龄:20
姓名:小红, 年龄:20

如果你熟悉 Java 语言,其实类属性就是 Java 的静态类成员变量。

示例 2:创建多个 Person 的实例,然后通过类名修改类属性的值,最后通过实例名打印信息,验证类属性是所有类实例共享的。

class Person:
    # 类属性
    name = "小红"
    age = 20

    # 打印信息
    def info(self):
        print(f"姓名:{self.name},年龄:{self.age}")

if __name__ == "__main__":
    p1 = Person() # 调用构造函数
    p1.info()

    # 修改类属性
    Person.name = "小明"
    Person.age = 18

    p2 = Person() # 调用构造函数

    # 打印信息
    p1.info()
    p2.info()

运行结果:

姓名:小红,年龄:20
姓名:小明,年龄:18
姓名:小明,年龄:18
  • 实例属性

实例属性是指定义在类的方法中、且绑定在 self 上的属性。例如:

class Person:

    # 构造函数
    def __init__(self):
        self.name = "小红"   # 实例属性
        self.age = 20       # 实例属性

    # 打印信息
    def info(self):
        print(f"姓名:{self.name},年龄:{self.age}")


if __name__ == "__main__":
    p1 = Person()   # 调用构造函数
    p2 = Person()

    p1.name = "小明"  # 使用实例名访问实例属性
    p2.age = 43

    p1.info()
    p2.info()

运行结果:

姓名:小明,年龄:20
姓名:小红,年龄:43

从上面输出结果可以知道,p1 修改 name,p2 修改 age 实例属性,均只影响当前实例,对其他实例没有影响。实例属性的特点:

  1. 实例属性仅属于某个具体对象。像 p1 = Person() 里,p1.name、p1.age 只属于 p1 这个实例。
  2. 每个实例互相独立。再创建一个 p2,它的 name、age 不会影响 p1。
  3. 实例属性 name 和 age 只在当前实例生效。

访问限制

在类的内部可以定义属性和方法,在类的外部则可以直接调用这些属性或方法来操作数据,从而隐藏类内部的复杂实现逻辑。不过,Python 本身并没有对属性和方法提供严格的访问权限控制。为了保护类内部的某些属性或方法不被外部随意访问、修改,可以在属性或方法名称前添加双下划线(如 __calc),或在名称首尾均添加双下划线,以此实现访问权限限制。

其中,双下划线的具体作用如下:

  • 首尾均带有双下划线的命名,用于表示特殊方法,通常由系统定义,如构造方法 __init__()。
  • 仅以双下划线开头的命名,用于表示私有成员。这类成员只允许在定义它的类内部访问,不能直接通过类的实例对象访问;但仍可以通过“实例名._类名__xxx”的形式间接访问。

示例:

class Person:
    __version = "1.0.1"   # 定义私有属性

    # 构造函数
    def __init__(self):
        print(f"version={self.__version}") # 在实例方法中访问私有属性


if __name__ == "__main__":
    p1 = Person()   # 调用构造函数
    print(p1._Person__version)  # 通过 “实例名._类名__xxx” 方式访问私有属性

运行结果:

version=1.0.1
1.0.1

属性(property)

这里介绍的属性和上面介绍的类属性和实例属性不同,而是一种比较特殊的属性,访问它时将计算它的值。另外,该属性还可以为属性添加安全保护机制。

在 Python 中,可以通过 @property(装饰器)将一个方法转换为属性。将方法转换为属性后,可以直接通过方法名来访问,而无需再添加一对小括号,这样代码更简单。

创建属性的语法:

@property
def methodName(self):
    block

参数说明:

  • methodName:用于指定方法名,一般使用小写字母开头。该名称最后将用作创建的属性名。
  • self:必要参数,表示类的实例。
  • block:方法体,实现方法的具体功能。在方法体中,通常以 return 语句结束,返回计算结果。

示例:下面将给出一个使用方法计算矩形周长和面积,然后再提供一个通过属性实现计算矩形周长和面积,比较两者的异同。

下面类通过方法的方式计算矩形的周长和面积:

class Rectangle:
    """
    矩形类
    """
    def __init__(self, width, height):
        # 初始化矩形的宽和高
        self.width = width
        self.height = height

    # 计算属性:面积(访问时自动计算)
    def area(self):
        return self.width * self.height

    # 计算属性:周长(访问时自动计算)
    def perimeter(self):
        return 2 * (self.width + self.height)


if __name__ == "__main__":
    rect = Rectangle(3, 4)

    # 访问属性(不用加括号!)
    print("宽:", rect.width)
    print("高:", rect.height)
    print("面积:", rect.area())  # 调用函数
    print("周长:", rect.perimeter())  # 调用函数

运行结果:

宽: 3
高: 4
面积: 12
周长: 14

下面类为上面代码的 area() 和 perimeter() 函数添加 @property 装饰器,代码如下:

class Rectangle:
    """
    矩形类
    """
    def __init__(self, width, height):
        # 初始化矩形的宽和高
        self.width = width
        self.height = height

    # 计算属性:面积(访问时自动计算)
    @property
    def area(self):
        return self.width * self.height

    # 计算属性:周长(访问时自动计算)
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)


if __name__ == "__main__":
    rect = Rectangle(3, 4)

    # 访问属性(不用加括号!)
    print("宽:", rect.width)
    print("高:", rect.height)
    print("面积:", rect.area)  # 使用属性
    print("周长:", rect.perimeter)  # 使用属性

运行结果:

宽: 3
高: 4
面积: 12
周长: 14

注意,不能对属性进行赋值,如果重新对属性赋值,例如:

class Rectangle:
    """
    矩形类
    """
    def __init__(self, width, height):
        # 初始化矩形的宽和高
        self.width = width
        self.height = height

    # 计算属性:面积(访问时自动计算)
    @property
    def area(self):
        return self.width * self.height

    # 计算属性:周长(访问时自动计算)
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)


if __name__ == "__main__":
    rect = Rectangle(3, 4)
    rect.area = 10		# AttributeError

将抛出“AttributeError: property 'area' of 'Rectangle' object has no setter”错误。

继承(extend)

在编写类时,并非每次都需要从零开始实现。如果待开发的类与已有类之间存在继承关系,就可以通过继承实现代码复用,从而有效提高开发效率。

继承的基本语法

继承是面向对象编程中最为核心的特性之一,它的设计思想来源于人类认识客观世界的逻辑,也是自然界中普遍存在的传递与演化规律。在现实生活里,我们每个人都会从祖辈与父母身上继承相貌、体型等诸多特征,但同时又不会和父母完全相同 —— 每个人都会形成独属于自己的性格、习惯与特点,这些专属特征并不会完全照搬自长辈,而是在继承基础上形成的独特差异。如下图所示:

在程序设计中实现继承,意味着子类(或派生类)会自动拥有其继承的父类(或基类)的所有公有成员和受保护成员,无需重复定义这些成员,从而实现代码的高效复用。

在面向对象编程的规范中,我们通常将被继承的原有类称为父类(也可称为基类、超类),而通过继承创建的新类则被称为子类(也可称为派生类)。

通过继承,我们不仅能够高效实现代码的重用,避免重复编写相同的逻辑和代码,还可以清晰理顺不同类之间的层级关系,让类的结构更具逻辑性和可读性,便于后续的维护与扩展。

在 Python 中,实现类的继承非常简洁,只需在类定义语句中,在类名右侧用一对小括号,将需要继承的基类名称包裹起来即可。其语法格式如下:

class ClassName(baseClassList):
    """类的帮助信息"""		# 类文档字符串
    statement				# 类体

参数说明:

  • ClassName:用于指定类名。
  • baseClassList:用于指定要继承的基类,可以有多个,类名之间用逗号“,”分割。如果不指定,将使用所有 Python 对象的根类 object。
  • statement:类体,主要由变量、方法和属性等语句组成。在定义类时,如果没想好类的具体功能,可以在类体中直接使用 pass 语句代替。

示例:先定义一个基类 Animal,然后定义 Dog 类,该类继承了 Animal 类。如下:

# 父类(基类):动物类
class Animal:
    def __init__(self, name, age):
        # 初始化属性
        self.name = name
        self.age = age

    # 父类方法:叫
    def speak(self):
        print(f"{self.name} 发出了声音")

    # 父类方法:介绍自己
    def info(self):
        print(f"我是 {self.name},今年 {self.age} 岁")


# 子类(派生类):狗类,继承自动物类
class Dog(Animal):
    def __init__(self, name, age, breed):
        # 调用父类的构造方法,继承父类的属性
        super().__init__(name, age)
        # 子类独有的属性:品种
        self.breed = breed

    # 方法重写(覆盖父类的speak方法)
    def speak(self):
        print(f"{self.name} 汪汪叫!")

    # 子类独有的方法
    def show_breed(self):
        print(f"{self.name} 的品种是:{self.breed}")


if __name__ == '__main__':
    # 创建子类对象
    dog = Dog("旺财", 3, "金毛")

    # 调用 继承自父类 的方法
    dog.info()          # 我是 旺财,今年 3 岁

    # 调用 子类重写 的方法
    dog.speak()         # 旺财 汪汪叫!

    # 调用 子类自己的方法
    dog.show_breed()    # 旺财 的品种是:金毛

    # 访问 继承自父类 的属性
    print("年龄:", dog.age)   # 年龄: 3

运行结果:

我是 旺财,今年 3 岁
旺财 汪汪叫!
旺财 的品种是:金毛
年龄: 3

从该运行结果可知,使用 Dog 类的实例调用了 Dog 中没有定义的 info() 方法,该方法来自父类,同时在 Dog 中重新实现了 speak() 方法,这就是方法重写,下面将详细介绍。

方法重写

基类中定义的属性和方法,都会被派生类直接继承使用。但在实际开发中,基类的某些方法往往只能实现通用逻辑,不一定完全适配派生类的具体需求。这时就需要在派生类中重新定义一个与父类同名、同参数的方法,覆盖掉从基类继承来的实现,这一机制称为方法重写。

这一特性与 Java 语言中的方法重写在设计思想和使用逻辑上是完全一致的。

示例:创建一个基类 Animal,然后创建 Dog 和 Cat 类继承 Animal 类,并且都重写 speak() 方法,如下:

# 父类(基类)
class Animal:
    def speak(self):
        print("动物发出声音")

# 子类(派生类)
class Dog(Animal):
    # 重写父类的 speak 方法
    def speak(self):
        print("小狗汪汪叫")

class Cat(Animal):
    # 同样重写 speak 方法
    def speak(self):
        print("小猫喵喵叫")

if __name__ == '__main__':
    dog = Dog()
    dog.speak()   # 输出:小狗汪汪叫

    cat = Cat()
    cat.speak()   # 输出:小猫喵喵叫

运行结果:

小狗汪汪叫
小猫喵喵叫

派生类中调用基类的 __init__() 方法

派生类如果定义了自己的 __init__(),不会自动调用基类的 __init__() 方法。基类中初始化的属性,就不会被赋值,派生类对象就无法使用。想要继承并使用基类的属性,必须手动调用基类的构造方法。

调用基类 __init__() 的方式有两种:

  • super().__init__(参数) —— 推荐方式
  • 基类名.__init__(self, 参数)

示例:定义一个基类 Person,创建一个派生类继承自 Student 类,然后在 Student 类的 __init__() 方法中手动调用父类的 __init__() 方法,如下:

# 基类
class Person:
    def __init__(self, name):
        self.name = name  # 基类初始化属性


# 派生类
class Student(Person):
    def __init__(self, name, score):
        # 手动调用基类的 __init__
        super().__init__(name)

        # 派生类自己的属性
        self.score = score

    def show(self):
        print(f"姓名:{self.name},分数:{self.score}")


if __name__ == "__main__":
    s = Student("小明", 90)
    s.show()

运行结果:

姓名:小明,分数:90

更多 Python3 知识,请继续学习后续章节。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号