Featured image of post python 类

python 类

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。

类属于面向对象编程。我理解是把数据流转中某些非常非常常见的自然的单元变成一个黑箱,内部的东西定义好了之后就不需要管了;和函数的区别是,类内部的生态和功能更加丰富,甚至可以有状态这种类似内环境的概念,这是函数一般没有的(除非使用闭包)。

定义(类的基本结构)

属性(数据/状态)

方法(行为/功能)

外界访问(封装/接口)

继承(代码复用)

多态(接口统一)

区分类和实例属性(内存和数据管理)

实例属性, 存储个体差异 如果用类属性存储颜色,所有汽车的颜色都会被最后一次赋值覆盖,数据将混乱。

类属性, 存储共享常量或默认值 如果用实例属性存储轮子数,虽然可行,但会浪费内存(例如每个对象都存一个 WHEELS=4),且无法方便地统一修改。

下列内容由廖雪峰python教程 以及本人对AI的提问思考 整理得到。 https://liaoxuefeng.com/books/python/introduction/index.html


定义(类的基本结构)

面向对象最重要的概念就是类(Class)和实例(Instance),类是抽象的模板,实例是根据类创建出来的一个个具体的“对象”。每个对象都拥有相同的方法,但各自的数据不同。

类的定义

定义类是通过class关键字:

class Student(object): pass

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。如果没有合适的继承类,就使用object类。Python 3.x (当前版本):所有类默认继承 object,可以不显式写出。

定义实例和init函数,self的绑定

定义实例的时候等同于调用init函数,只需要这样写bart = Student() 。

可以在类定义的时候就绑定属性和方法,通过__init__函数/ __init__方法的第一个参数永远是self,表示创建的实例本身。在__init__方法内部,可以把各种属性绑定到self。 创建实例的时候必须传入与__init__方法匹配的参数,除去self不需要传(将实例与函数绑定(自动传入 self))

init函数可以有默认值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class DataLoader:
#类的定义可以包含一些默认的类属性(如果需要;见这里的7)
DEFAULT_BATCH_SIZE = 1 
# 构造函数 __init__ 负责接收和设置参数
def __init__(
    self, 
    dataset,           # 1. 必需参数(没有默认值)
    batch_size=1,      # 2. 可选参数,带有默认值
    shuffle=False,     # 3. 可选的布尔值参数
    num_workers=0,     # 4. 可选的数值参数
    collate_fn=None,   # 5. 可选的函数/对象参数,默认 None
    *args,             # 6. 捕获未命名位置参数(不常见于 DataLoader)
    **kwargs           # 7. 捕获未命名关键字参数(不常见于 DataLoader)
):
    # 核心步骤:将接收到的参数(或默认值)赋值给实例属性

    # 必需参数必须赋值
    self.dataset = dataset
    
    # 可选参数赋值
    self.batch_size = batch_size
    self.shuffle = shuffle
    self.num_workers = num_workers
    self.collate_fn = collate_fn
    
    # 可以在这里对默认值进行逻辑处理
    if self.collate_fn is None:
        # 如果用户没提供,可以设置一个内部的默认函数
        self.collate_fn = self._default_collate_function 
        
    # 存储其他捕获的参数
    self.extra_args = args
    self.extra_kwargs = kwargs

def _default_collate_function(self, batch):
    """内部默认的拼接逻辑"""
    print("使用默认的拼接函数。")
    return batch # 简化处理

def load_data(self):
    # 使用实例属性来控制行为
    print(f"加载数据中: batch_size={self.batch_size}, num_workers={self.num_workers}, shuffle={self.shuffle}")
    self.collate_fn(["sample1", "sample2"])

实例和类的区别

实例和类不一样。

bart = Student() bart <main.Student object at 0x10a67a590> Student <class ‘main.Student’>


type用于定义类

type也可以用于定义类,type(name, bases, dict),name: 类的名称(字符串)。bases: 继承的基类(元组,对应类的 bases 特殊属性)。dict: 类的属性和方法(字典,对应类的 dict 特殊属性)。

type定义类示例


类本身也有type,和实例不一样


$#$ 等价于 class Hello: pass

Hello = type(‘Hello’, (), {}) h = Hello() print(type(h)) # <class ‘main.Hello’> print(type(Hello)) # <class ’type’>

实例 h └─ 属于类 Hello(type(h) == Hello)

类 Hello └─ 属于类型 type(type(Hello) == type)

元类 type └─ 属于自己(type(type) == type)


type用于判断对象类型

在 Python 中,“一切皆对象”,type可以创造对象=也可以进行类型检验

type(123) <class ‘int’> type(‘str’) <class ‘str’> type(None) <type(None) ‘NoneType’>


type(123)==int True

type区分得很详细

type(abs) <class ‘builtin_function_or_method’> type(a) <class ‘main.Animal’>

基本类型都可以用type()判断。

涉及函数和生成器的,需要import types

types模块使用示例

类也是一个数据类型

当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

a = list() # a是list类型 b = Animal() # b是Animal类型 c = Dog() # c是Dog类型 判断一个变量是否是某个类型可以用isinstance()判断:

isinstance(a, list) True isinstance(b, Animal) True isinstance(c, Dog) True

isinstance和type区别

type() 检查类型是否精确相等,而 isinstance() 考虑继承关系。

特征type(obj)isinstance(obj, classinfo)
功能返回对象的精确类型(类)。检查对象是否是指定类或其基类(父类)的实例。
继承性不考虑继承。只有类型完全相同时才返回 True。考虑继承。如果对象是子类实例,也会被视为父类实例,返回 True。
参数只需要一个参数:obj。需要两个参数:obj 和 classinfo(可以是单个类或类型元组)。
返回值返回一个 type 对象(即对象的类)。返回布尔值 True 或 False。
使用场景调试、内省、元编程(例如动态创建类)、或严格要求排除子类时。推荐的类型检查方式。适用于运行时类型检查、鸭子类型(Duck Typing)和确保代码支持多态性。


属性(数据/状态)

可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性。

bart.name = ‘Bart Simpson’

这个属性可以随便绑定,不需要在类定义时候就绑定。对于这个属性的数值也可以随意更改。

私有属性

如果想要不被外界直接访问的属性,在类的定义里面把变量名定义成以__开头,就变成了一个私有变量(private)。此时外界无法直接访问

bart.__name Traceback (most recent call last): File “”, line 1, in AttributeError: ‘Student’ object has no attribute ‘__name’

不能直接访问__name是因为Python解释器对外把__name变量改成了_Student(类名)__name,所以,仍然可以通过_Student__name来访问__name变量。但最好不要这样做。

bart._Student__name ‘Bart Simpson’

如果类里面没有定义,而是在实例里面定义,bart,__name=‘Tom’,可以吗。此时创建的并不是一个真正的私有属性,并不等于类里面的这个变量。


特殊属性

以双下划线开头,并且以双下划线结尾的是特殊变量,是可以直接访问的,不是private变量。例如type函数访问的时候就用了__class__ 属性.



以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,约定俗成,不要随意访问。


方法(行为/功能)

定义类的方法

没有必要从外面的函数去访问类的数据,可以直接在类的内部定义封装数据的函数,即类的方法。

第一个参数需要是self表示类的实例,在调用时候可以不写,其他等同于正常函数

def print_score(self): print(’%s: %s’ % (self.name, self.score))


定义方法以处理私有属性更改

对于类里面的私有属性的更改,可以定义方法,此时可以对参数进行检查,防止随便赋值

def set_score(self, score): self.__score = score


定义特殊方法

以双下划线开头,并且以双下划线结尾的是特殊方法,在某些操作的时候自动调用,例如init就是在创造类的时候自动调用的,再例如__getitem__(self, key) ,允许使用方括号取值(例如 obj[key]),再例如__iter__(self) , 在 for item in obj: 循环开始时,使对象成为可迭代对象。

我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:

__len__方法示例

call 方法

当一个类的实例被当作函数一样使用圆括号 () 调用时,Python 解释器会自动查找并执行该实例的 call 方法。对于类的实例 ff,您可以用 ff(…) 的形式调用它,这等价于执行 ff.call(…)。

call 方法的主要作用是让一个类的实例变得可调用(Callable),就像函数一样。如果一个对象实现了 call 方法,那么这个对象被称为 可调用对象(Callable Object)。

兼容只接受普通函数的框架接口(多态性),可以在任何期望传入函数的地方传入。

call 方法是操作的执行者,并且还是有状态的(普通的函数没有状态和内部变量,类是有的),写程序时候更简洁,执行阶段只需关注输入和输出。

将复杂的初始化和简单的执行清晰地划分为两个阶段:配置阶段(init): 接收所有配置参数,如模型路径、最大长度、数据格式等,并将它们存储在 self 属性中。执行阶段(call): 只关注核心逻辑的执行,直接使用 self 中保存的配置,无需重新接收这些配置参数,由于状态保存在实例中,可以实现跨多次调用的复杂逻辑。

可以基于同一个类,创建出功能相似但配置完全不同的多个实例,并将它们用作不同的函数:

__call__方法示例


获取对象的所有属性和方法

使用dir()函数,它返回一个包含字符串的list

dir(‘ABC’) [’add’, ‘class’,…, ‘subclasshook’, ‘capitalize’, ‘casefold’,…, ‘zfill’]

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

len函数等价调用示例


配合hasattr() getattr()以及setattr(),我们可以直接操作一个对象的状态。这些函数是所有type的对象都有的.


可以看这个对象是否有某个属性,获取某个属性的值,并且给其设置值为xx的属性(obj.y=xx是直接赋值,属性名必须在编写代码时是确定的,setattr(obj, ’name’, value)可以动态变化属性名,只有在运行时才知道具体是什么)

属性和方法的区别

属性和方法的区别大吗。不大。从技术层面看,属性是数据,方法是函数。但在 Python 中,它们都统一地存储在类的命名空间中,并都可以使用 getattr()、setattr() 等函数以字符串名称的方式进行动态操作。方法通过绑定机制,最终也会被视为一种可以获取和调用的“特殊属性”。

fn = getattr(obj, ‘power’) # 获取属性’power’并赋值到变量fn,实际上这是个函数 fn # fn指向obj.power <bound method MyObject.power of <main.MyObject object at 0x10077a6a0» fn() # 调用fn()与调用obj.power()是一样的 81

方法和属性最大的区别是有没有括号,有没有和参数绑定,所以可以使用 setattr() 给对象设置一个等效于函数的属性,但需要处理好 self 参数。

给类/实例添加方法

不处理self参数,每次用需要手动传入实例而不能像self一样略去

def my_function(instance): return f"Value of x is {instance.x}"

setattr(obj, ‘ff’, my_function)

print(obj.ff(obj))

添加self,把这个函数添加到类的方法里面,对这个类的所有实例都生效

这里需要 定义一个接受 self 的函数;Python 的描述符协议会自动处理绑定(将实例与函数绑定(自动传入 self))

def my_method(self, multiplier): return self.x * multiplier

setattr(MyClass, ‘ff’, my_method)

print(obj1.ff(5)) # 输出: 50

只给类的实例添加方法,对这个类的其他实例不适用

def my_method(self, multiplier): return self.x * multiplier

关键步骤:使用 MethodType 绑定实例 (obj) 和函数 (my_method) bound_method = types.MethodType(my_method, obj)

setattr(obj, ‘ff’, bound_method)


4外界访问(封装/接口)

合并在类的方法和属性里面了

继承(代码复用)

当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。



class Animal(object): def run(self): print(‘Animal is running…’)

class Dog(Animal): pass

dog = Dog() dog.run()

Animal is running…

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run(),哪怕run的参数格式不符合子类的格式而符合父类的格式,此时也只会报错而不是运行父类的run()。


在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行

b = Animal() isinstance(b, Dog) False

import类之后的调用

例子,import一个类之后,就可以像调用函数一样直接写:

from torch.utils.data import DataLoader

dataset = MyDataset([1,2,3,4,5,6], [0,1,0,1,0,1]) loader = DataLoader(dataset, batch_size=2, shuffle=True)

for batch in loader: print(batch)

也可以参数更多地写成这样,loader = DataLoader( dataset, # 必填:你的 Dataset 实例(map-style 或 iterable-style) batch_size=1, # 每个 batch 里样本数 shuffle=False, # 是否在每个 epoch 打乱(仅对基于长度的采样器有效) sampler=None, # 自定义样本索引的产生方式(与 shuffle 互斥) batch_sampler=None,# 直接产生“一个批次的索引列表”(与 batch_size/sampler 互斥) num_workers=0, # 额外开启多少个子进程并行加载(>0 需注意 Windows/随机数种子等) collate_fn=None, # 如何把若干样本“拼成一个 batch”,默认能处理张量/数值/字典/列表 drop_last=False, # 数据集大小不是 batch_size 的整数倍时,是否丢弃最后一个小批次 pin_memory=False, # 把 batch 固定在页锁内存以加速拷贝到 GPU(适合 CUDA 训练) )


class torch.utils.data.Dataset(object) 是一个类定义声明或文档引用,而不是一个完整的、可执行的语句块。

init的继承,super()

super().init(name):

子类 Child 的 init 会完全覆盖父类 Parent 的 init。创建 Child 实例时,self.name 将不会被初始化,可能会导致后续代码出错。


super() ,是遵循 MRO(方法解析顺序),在子类中调用父类(或更高层基类)的方法。

当你在一个复杂的多重继承体系中时,一个类可能有多个父类。Python 有一个明确的规则来决定方法的查找顺序,这个规则称为 方法解析顺序 (Method Resolution Order, MRO)。

print(B.mro):(<class ‘main.B’>, <class ‘main.A’>, <class ‘object’>)

这里Python 自动帮你推断当前类是谁、当前实例是谁,因此不需要像python2那里写参数,例如super(Dog, self).run();super() 返回的是一个 对象(type:<class ‘super’>,isinstance(s, super)),本身是和当前对象绑定的;super().some_method()从当前类的下一个类开始往后查找 some_method;

class A: def hi(self): print(“A”) class B(A): def hi(self): print(“B”); super().hi() class C(A): def hi(self): print(“C”); super().hi() class D(B, C): def hi(self): print(“D”); super().hi()

D 的 MRO (方法解析顺序): [D, B, C, A, object]

d = D() d.hi()

这里是依赖上下文的语法糖。这种省略参数的机制,被称为零参数 super(),它之所以常见,是为了实现代码的简洁和可维护性。Python 中有很多依赖于上下文来省略参数的例子,目标都是为了简洁

多态(接口统一)

def run_twice(animal): #这里要求animal是任意有run这个方法的实例对象 animal.run() animal.run()

run_twice(Animal()),run_twice(Dog()),都可以运行


对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,

这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。


对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。



区分类和实例属性(内存和数据管理)

类属性和实例属性的区别

s = Student(‘Bob’) s.score = 90

这种是实例方法,之前知道的。

self代表实例,cls代表类

class Student: name = ‘Student’ # 类属性,实例没有这个属性

1
2
def __init__(self, name):
    self.name = name  # 实例属性,因为和self有关

print(Student.name) # 输出: Student (类属性) s = Student(‘Tom’) print(s.name) # 输出: Tom (实例属性) print(Student.name) # 输出: Student (类属性不受影响)


实例属性 s.name 会遮蔽同名的类属性。

但类属性 Student.name 仍然存在于类中,不受实例影响。

当你删除实例属性后,再使用相同的名称,访问到的将是类属性。


可以给类而不是实例定义方法

可以给类而不是实例定义方法,但需要使用 @classmethod 装饰器,并且第一个参数必须是 cls

@classmethod def change_school(cls, new_name): “““类方法:使用 cls 访问并修改类属性””” cls.SCHOOL_NAME = new_name print(f"学校名称已更新为: {cls.SCHOOL_NAME}")


静态方法

也有既不接收 self(实例)参数,也不接收 cls(类)参数的静态方法

@staticmethod def add(a, b): “““静态方法:与类或实例状态无关的工具函数””” return a + b

为什么实例的定义比类定义更简洁自然

定义实例属性和实例方法之所以看起来更简洁、更自然,是因为它们是面向对象编程中最基础、最核心的部分,是 Python 默认的设计模式。

而定义类属性和类方法需要额外的步骤(例如 init 外的直接赋值或使用装饰器),这反映了它们在 OOP 层次结构中的特定角色。

实例(对象)是 OOP 最基本的单位,实例属性和方法是 Python 面向对象设计的“标准”和“默认”行为。

实例方法利用了默认的实例绑定机制,所以无需装饰器。

实例属性利用了实例内部的动态字典存储机制,所以可以在 init 中方便地定义和赋值。

而类方法和类属性,由于其共享性或特殊的绑定需求,需要额外的语法(如 @classmethod 或在 init 外定义)来区分它们,从而打破了默认的实例模式。