【设计模式】原型模式

三、原型模式

3.2 原型模式

同工厂模式一样,原型(Prototype) 模式也是一种创建型模式。原型模式通过一个对象 (原型对象)克隆出多个一模一样的对象。实际上,该模式与其说是一种设计模式,不如说是 一种创建对象的方法(对象克隆),尤其是创建给定类的对象(实例)过程很复杂(例如,要设 置许多成员变量的值)时,使用这种设计模式就比较合适。

3.2.1 通过工厂方法模式演变到原型模式

回顾一下前面讲解工厂方法模式时的范例,由图3.2,可以看到:

  • 怪物相关类 M_Undead 、M_Element 、M_Mechanic 分别继承自怪物父类Monster;
  • 怪物工厂相关类 M_UndeadFactory 、M_ElementFactory 、M_MechanicFactory 分 别 继承自工厂父类 M_ParFactory;
  • 怪物工厂类 M_UndeadFactory 、M_ElementFactory 、M_MechanicFactory 中的成员 函数createMonster 分别用于创建怪物类 M_Undead 、M_Element 、M_Mechanic 对 象 。

现在,把上述类的层次结构(源码)进行一下变换,请读者仔细观察:

  • (1)把怪物父类 Monster 和工厂父类 M_ParFactory 合二为一 (或者说成是把 M_ParFactory 类中的能力搬到 Monster 中去并把 M_ParFactory 类删除掉),让怪物父类 Monster 本身具有克隆自己的能力。改造后的代码如下:
class Monster
{public:Monster(int life,int magic,int attack);virtual ~Monster();virtual Monster* createMonster() = 0;protected:int m_life;int m_magic;int m_attack;
};Monster::~Monster(){}
  • (2)遵从传统习惯(但不是一定要这样做),将上述成员函数createMonster 重新命名为 clone,clone 的中文翻译为“克隆”,意味着调用该成员函数就会从当前类对象复制出一个完 全相同的对象(通过克隆自己来创建出新对象),这当然也是一种创建该类所属对象的方式,虽然读者可能以往没见过这种创建对象的方式,但相信在将来阅读大型项目的源码时会遇 到这种创建对象的方式。改造后的代码如下:
virtual Monster* clone() = 0;
  • (3)把 M_UndeadFactory、M_ElementFactory、M_MechanicFactory 这3个怪物工厂 类中的createMonster 成员函数分别搬到M_Undead 、M_Element 、M_Mechanic 中并将该成 员函数重新命名为clone, 同时将M_UndeadFactory 、M_ElementFactory 、M_MechanicFactory类 删除掉。改造后的代码如下:

class M_Undead :public Monster {public:M_Undead(int life, int magic, int attack);virtual Monster* clone() override;
};class M_Element :public Monster {public:M_Element(int life, int magic, int attack);virtual Monster* clone() override;};class M_Mechanic :public Monster {public:M_Mechanic(int life, int magic, int attack);virtual Monster* clone() override;
};
  • (4)当然,既然是克隆,那么上述M Undead 、M Element 、M Mechanic 中 的clone 成员 函数的实现体是需要修改的。例如,某个机械类怪物因为被主角砍了一刀失去了100点生 命值,导致该怪物对象的m life 成员变量(生命值)从原来的400变成300,那么调用 clone 方法克隆出来的新机械类怪物对象也应该是300点生命值,所以此时 M_Mechanic 类中 clone 成员函数中的代码行
return new M_Mechanic(400,0,110);

就不合适,因为这样会 创建(克隆)出一个400点生命值的新怪物,不符合 clone这个成员函数的本意(复制出一个 完全相同的对象)。

克隆对象自身实际上是需要调用类的拷贝构造函数的。阅读过笔者的《C++ 新经典: 对象模型》的读者都知道:

  • ① 如果程序员在类中没有定义自己的拷贝构造函数,那么编译器会在必要的时候(但 不是一定)合成出一个拷贝构造函数;
  • ② 在某些情况下,程序员必须书写自己的拷贝构造函数,例如在涉及深拷贝的情形之 下,如果读者对深拷贝和浅拷贝这两个概念理解模糊,建议一定要通过搜索引擎或者《C++新 经典:对象模型》这本书理解清楚,因为这决定着你能否写出正确的程序代码。克隆对象意 味着复制出一个全新的对象,所以在涉及深拷贝和浅拷贝概念时都是要实现深拷贝的(这样 后续如果需要对克隆出来的对象进行修改才不会影响原型对象)。

为方便查看测试结果,笔者为M Element 类编写了一个拷贝构造函数供读者参考,在 M_Element 中,加入如下代码:

M_Element::M_Element(M_Element const& tmpobj):Monster(tmpobj)
{std::cout << "调用了M_Element的拷贝构造函数" << std::endl;
}Monster* M_Element::clone() {return new M_Element(*this);
}

对 M_Undead 、M_Element 、M_Mechanic 中的clone 成员函数的实现体分别进行修改, 通过调用类的拷贝构造函数的方式真正实现类对象的克隆,修改后的代码如下

class Monster
{public:Monster(int life, int magic, int attack);virtual ~Monster();virtual Monster* clone() = 0;protected:int m_life;int m_magic;int m_attack;
};class M_Undead :public Monster {public:M_Undead(int life, int magic, int attack);M_Undead(const M_Undead& tmpobj);virtual Monster* clone() override;
};class M_Element :public Monster {public:M_Element(int life, int magic, int attack);M_Element(M_Element const& tmpobj);virtual Monster* clone() override;};class M_Mechanic :public Monster {public:M_Mechanic(int life, int magic, int attack);M_Mechanic(M_Mechanic const& tmpobj);virtual Monster* clone() override;};Monster::Monster(int life, int magic, int attack):m_life(life), m_magic(magic), m_attack(attack)
{}Monster::~Monster()
{
}M_Undead::M_Undead(int life, int magic, int attack):Monster(life, magic, attack)
{std::cout << "一只亡灵怪物来到了这个世界" << std::endl;
}M_Undead::M_Undead(const M_Undead& tmpobj):Monster(tmpobj)
{std::cout << "调用M_Undead的拷贝构造函数" << std::endl;
}Monster* M_Undead::clone() {return new M_Undead(*this);
}M_Element::M_Element(int life, int magic, int attack):Monster(life, magic, attack)
{std::cout << "一只元素怪物来到了这个世界" << std::endl;
}M_Element::M_Element(M_Element const& tmpobj):Monster(tmpobj)
{std::cout << "调用了M_Element的拷贝构造函数" << std::endl;
}Monster* M_Element::clone() {return new M_Element(*this);
}M_Mechanic::M_Mechanic(int life, int magic, int attack):Monster(life, magic, attack)
{std::cout << "一只机械怪物来到了这个世界" << std::endl;
}M_Mechanic::M_Mechanic(M_Mechanic const& tmpobj):Monster(tmpobj)
{std::cout << "调用了M_Mechanic的拷贝构造函数" << std::endl;
}Monster* M_Mechanic::clone() {return new M_Mechanic(*this);
}
  • (5)如果在上述(4)中确定要编写自己的拷贝构造函数,应确保无误。因为只有拷贝构 造函数正确,调用clone 成员函数时才能正确地克隆出新的对象。

  • (6)在实际项目中,可以先创建出原型对象,原型对象一般只是用于克隆目的而存在, 然后就可以调用这些对象所属类的clone 成员函数来随时克隆出新的对象,并通过这些新 对象实现项目的业务逻辑。在main 主函数中,增加如下测试代码:

  Monster* pmyPropUndMonster = new M_Undead(300, 50, 80);Monster* pmyPropEleMonster = new M_Element(200, 80, 100);M_Mechanic myPropMecMonster(400, 0, 110);Monster* p_CloneObj1 = pmyPropUndMonster->clone();Monster* p_CloneObj2 = pmyPropEleMonster->clone();Monster* p_CloneObj3 = myPropMecMonster.clone();delete p_CloneObj1;delete p_CloneObj2;delete p_CloneObj3;delete pmyPropUndMonster;delete pmyPropEleMonster;

从代码中可以看到,分别在栈和堆上创建了一个原型对象以用于克隆的目的,当然,在 堆中创建的原型对象最终不要忘记释放对应的内存以防止内存泄漏。甚至可以根据项目的 需要,将多个原型对象保存在例如 map 容器中,甚至可以书写专门的管理类来管理这些原 型对象,当需要用这些原型对象创建(克隆)新对象时,可以从容器中取出来使用。在对原型 对象的使用方面,程序员完全可以发挥自己的想象力。

3.2.2 引入原型模式

在前面的范例中,原型对象通过clone 成员函数调用怪物子类的拷贝构造函数,可能有 些读者认为这有些多余——直接利用怪物子类的拷贝构造函数生成新对象不是更直接、更方便吗?例如在main 主函数利用代码行

Monster* p_CloneObj3 = new M_Mechanic(myPropMecMonst);

也可以克隆出一个新的对象。其实这样认为也没错,但读者要认识 到,设计模式是独立于计算机编程语言而存在的,这意味着虽然 C++ 语言中怪物子类的 clone 成员函数可以直接调用拷贝构造函数,但在其他计算机编程语言中可能并没有拷贝构 造函数这种概念,此时,本该在拷贝构造函数中的实现代码就必须放在clone 成员函数中实 现 了 。

引入“原型”(Prototype)模式的定义:用原型实例指定创建对象的种类,并且通过复制 这些原型创建新的对象。简单来说,就是通过克隆来创建新的对象实例。

前面范例中的main 主函数中,myPropMecMonster 对象就是原型实例,通过调用该对象的clone 成员函数就指定了所创建的对象种类——当然,创建的是M Mechanic 类型的 对象而不是M_Undead 或 M_Element 类型的对象。通过复制myPropMecMonster 这个原 型对象以及 pmyPropEleMonster 所指向的原型对象创建了两个新对象: 一个是机械类怪 物对象,一个是元素类怪物对象,指针 p_CloneObjl 和 p_CloneObj2 分别指向这两个新对象。

针对前面的代码范例绘制原型模式的 UML 图,如图3.7所示。

Monster
# m_life
# m_magic
# m_attack
+Monster()
+~Monster()
+clone()
M_Undead
+M_Undead()
+clone()
M_Element
+M_Element()
+clone()
M_Mechanic
+M_Mechanic()
+clone()

原型模式的 UML 图中,包含两种角色。

  • (1)Prototype (抽象原型类):所有具体原型类的父类,在其中声明克隆方法。这里指 Monster 类。
  • (2)ConcretePrototype (具体原型类):实现在抽象原型类中声明的克隆方法,在克隆方法 中返回自己的一个克隆对象。这里指M_Undead 类 、M_Element 类 和M_Mechanic 类。

和工厂方法模式相比,原型模式有什么明显的特点呢?什么情况下应该采用原型模式 来克隆对象呢?
设想一下,如果某个对象的内部数据比较复杂且多变,例如一个实际游戏中的怪物 对 象 :

  • 在战斗中它的生命值可能因为被玩家攻击而不断减少;
  • 如果这个怪物会魔法,那么它施法后自身的魔法值也会减少;
  • 在生命值过低时怪物还可能自己使用一些药剂类物品或者治疗类魔法来替自己增 加生命值;
  • 玩家也可能通过施法导致怪物产生一些负面效果,例如中毒会持续让怪物丢失生命 值、混乱会让怪物乱跑而无法攻击玩家、石化导致怪物完全原地不动若干秒等。

如果使用工厂方法模式创建这种怪物对象(战斗中的,自身生命值、魔法值、状态等数据 随时在变化的怪物对象),那么大概要执行的步骤是:

  • 先通过调用工厂方法模式中的 createMonster 创建出该怪物对象(实际上就是创建
    一个怪物对象);
  • 通过怪物所属类中暴露出的设置接口(成员函数)来设置该怪物当前的生命值、魔法 值、状态(例如,中毒、混乱、石化)等,这些程序代码可能会比较烦琐。

显然,在这种情形下,使用工厂模式创建当前这个怪物对象就不如使用克隆方式来克隆 当前怪物对象容易,如果采用克隆方式来克隆当前对象,仅仅需要调用clone 成员函数,那 么因为clone 调用的实际是类的拷贝构造函数,所以这个怪物对象当前的内部数据(生命 值、魔法值、状态等)都会被立即克隆到新产生的对象中而不需要程序员额外通过程序代码 设置这些数据,也就是说,在调用clone 成员函数的那个时刻,克隆出来的对象与原型对象 内部的数据是完全一样的。例如,当游戏中的一个 BOSS 级别的怪物被攻击失血到一定程 度时,它会产生自己的分身,这种情况下使用clone 成员函数来产生这个分身就很合适,当 然,一旦新的对象被克隆出来后依旧可以单独设置该克隆对象自身的数据而丝毫不会影响 原型对象。

所以,如果对象的内部数据比较复杂且多变并且在创建对象的时候希望保持对象的当 前的状态,那么用原型模式显然比用工厂方法模式更合适。

总结一下工厂方法模式和原型模式在创建对象时的异同点:

  • 在前面范例中创建怪物对象时,这两种模式其实都不需要程序员知道所创建对象所 属的类名;

  • 工厂方法模式是调用相应的创建接口,例如使用createMonster 接口来创建新的怪 物对象,该接口中采用代码行“new 类名(参数…);”来完成对象的最终创建工作,这 仍旧是属于根据类名来生成新对象;

  • 原型模式是调用例如 clone(程序员可以修改成任意其他名字)接口来创建新的怪物 对象,按照惯例,这个接口一般不带任何参数,以免破坏克隆接口的统一性。该接口 中采用的是代码行“new 类名(* this);” 完成对类拷贝构造函数的调用来创建对象, 所以这种创建对象的方式是根据现有对象来生成新对象。

当然,有些读者把原型模式看成是一种特殊的工厂方法模式(工厂方法模式的变体),这 也是可以的——把原型对象所属的类本身(例如,M_Undead、M_Element、M_Mechanic) 看 成是创建克隆对象的工厂,而工厂方法指的自然就是克隆方法(clone)。

看一看原型模式的优缺点:

  • (1)如果创建的新对象内部数据比较复杂且多变,那么使用原型模式可以简化对象的 创建过程,提高新对象的创建效率。设想一下,如果对象内部数据是通过复杂的算法(例如 通过排序、计算哈希值等)计算得到,或者是通过从网络、数据库、文件中读取得到,那么用原 型模式从原型对象中直接复制生成新对象而不是每次从零开始创建全新的对象,对象的创 建效率显然会提高很多。

  • (2)通过观察图3.2(工厂方法模式UML 图)可以发现,工厂方法模式往往需要创建一 个与产品等级结构(层次)相同的工厂等级结构,这当然是一种额外的开销,而原型模式不存 在这种额外的等级结构——原型模式不需要额外的工厂类,只要通过调用类中的克隆方法 就可以生产新的对象。

  • (3)在产品类中,必须存在一个克隆方法以用于根据当前对象克隆出新的对象(加重开发者负担,这算是缺点)。当然,不一定采用“new 类名(* this);” 的形式来调用所属类的拷 贝构造函数实现对原型对象自身的克隆,也可以采用“new 类名(参数…);”先生成新的对 象,然后通过调用类的成员函数、直接设置成员变量等手段把原型对象内部的所有当前数据 赋给新对象,例如,可以把 M_Undead 的 clone 成员函数实现成下面的样子:

M_Undead::M_Undead(const M_Undead& tmpobj):Monster(tmpobj)
{Monster* pmonster = new M_Undead(300, 50, 80);pmonster->setLife(m_life);pmonster->setMagic(m_magic);pmonster->setAttack(m_attack);std::cout << "调用M_Undead的拷贝构造函数" << std::endl;
}
  • (4)在某些情况下,产品类中存在一个克隆方法也会给开发提供一些明显的便利。设 想一个全局函数Gbl CreateMonster, 其形参为Monster* 类型的指针,如果期望创建一 个与该指针所指向的对象相同类型的对象,那么传统做法的代码可能如下:
void Gbl_CreateMonster(Monster *pMonster);void Gbl_CreateMonster(Monster* pMonster) {Monster* ptmpobj = nullptr;if (dynamic_cast<M_Undead*>(pMonster)) {ptmpobj = new M_Undead(300, 50, 80);}if (dynamic_cast<M_Element*>(pMonster)) {ptmpobj = new M_Element(200, 80, 100);}if (dynamic_cast<M_Mechanic*>(pMonster)) {ptmpobj = new M_Mechanic(300, 0, 110);}if (ptmpobj != nullptr) {std::cout << "实现相关业务" << std::endl;delete ptmpobj;}
}

但是,如果每一个Monster 子类(M_Undead 、M_Element 、M_Mechanic) 都提供一个克 隆方法,那么Gbl CreateMonster函数的实现就简单得多,此时根本不使用dynamic_cast 和通过类名进行类型判断就可以直接利用已有对象来创建新对象,新的实现代码如下:

void Gbl_CreateMonster(Monster *pMonster);void Gbl_CreateMonster(Monster* pMonster)
{Monster* ptmpobj = pMonster->clone();std::cout << "实现相关业务" << std::endl;delete ptmpobj;
}

从这个范例中不难看到,根本就不需要知道Gbl_CreateMonster 的形参 pMonster 所 指向的对象到底是什么类型就可以创建出新的该形参所属类型的对象,这也减少了Gbl_CreateMonster 函数中需要知道的产品类名的数目。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/33803.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 文件权限类

目录 文件属性 从左到右的10个字符表示 rwx作用文件和目录的不同解释 图标&#xff1a; 案例实操 chmod 改变权限 基本语法 经验技巧 案例实操 拓展&#xff1a;可以通过一个命令查看用户列表 chown改变所有者 基本语法 选项说明 案例实操 chgrp 改变所属组 基…

DeepSeek技术解析:MoE架构实现与代码实战

以下是一篇结合DeepSeek技术解析与代码示例的技术文章&#xff0c;重点展示其核心算法实现与落地应用&#xff1a; DeepSeek技术解析&#xff1a;MoE架构实现与代码实战 作为中国AI领域的创新代表&#xff0c;DeepSeek在混合专家模型&#xff08;Mixture of Experts, MoE&…

vue3:八、登录界面实现-页面初始搭建、基础实现

一、初始工作 1、创建登录文件 在src/views中创建文件LoginView.vue文件 2、创建路由 在router/index.js中增加登录的信息 代码 import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue const router createRouter({hist…

dify+mysql的诗词助手

目录 数据库表结构&#xff1a; 数据库查询的http服务搭建&#xff1a; 流程引擎搭建&#xff1a; 开始&#xff0c; HTTP查询数据库&#xff0c; LLM数据分析&#xff0c; 直接回复&#xff0c; 效果测试&#xff1a; 下载链接&#xff1a; 数据库表结构&#xff1a;…

jenkins 配置邮件问题整理

版本&#xff1a;Jenkins 2.492.1 插件&#xff1a; A.jenkins自带的&#xff0c; B.安装功能强大的插件 配置流程&#xff1a; 1. jenkins->系统配置->Jenkins Location 此处的”系统管理员邮件地址“&#xff0c;是配置之后发件人的email。 2.配置系统自带的邮件A…

谷歌Chrome或微软Edge浏览器修改网页任意内容

在谷歌或微软浏览器按F12&#xff0c;打开开发者工具&#xff0c;切换到console选项卡&#xff1a; 在下面的输入行输入下面的命令回车&#xff1a; document.body.contentEditable"true"效果如下&#xff1a;

blender使用初体验(甜甜圈教程)

使用blender 建模了甜甜圈&#xff0c;时间空闲了&#xff0c;但愿能创建点好玩的吸引人的东西

Adobe Firefly 技术浅析(三):GANs 的改进

生成式对抗网络(GANs)在图像生成领域取得了显著的进展,但原始的 GANs 在训练稳定性、生成质量以及多样性方面存在一些挑战。Adobe Firefly 在其图像生成技术中采用了多种改进的 GANs 方法,以提高生成图像的质量和多样性。 1. 条件生成式对抗网络(cGANs) 1.1 基本原理 …

go GRPC学习笔记

本博文源于笔者正在学习的gprc&#xff0c;相关配套书籍素材来源是《Go编程进阶实战》&#xff0c;博文内容主要包含了RPC模式讲解&#xff0c;RPC通过htttp访问、拦截器、提高服务端与客户端容错的内容配置 在此之前需要下载protoc&#xff0c;这里不做下载过程 1、RPC模式 …

CentOS 系统安装 docker 以及常用插件

博主用的的是WindTerm软件链接的服务器&#xff0c;因为好用 1.链接上服务器登入后&#xff0c;在/root/目录下 2.执行以下命令安装docker sudo yum install -y yum-utilssudo yum-config-manager \--add-repo \https://download.docker.com/linux/centos/docker-ce.reposudo…

MindGYM:一个用于增强视觉-语言模型推理能力的合成数据集框架,通过生成自挑战问题来提升模型的多跳推理能力。

2025-03-13&#xff0c;由中山大学和阿里巴巴集团的研究团队提出了MindGYM框架&#xff0c;通过合成自挑战问题来增强视觉-语言模型&#xff08;VLMs&#xff09;的推理能力。MindGYM框架通过生成多跳推理问题和结构化课程训练&#xff0c;显著提升了模型在推理深度和广度上的表…

论文阅读笔记——LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS

LoRA 论文 传统全面微调&#xff0c;对每个任务学习的参数与原始模型相同&#xff1a; m a x Φ ∑ ( x , y ) ∈ Z ∑ t 1 ∣ y ∣ l o g ( P Φ ( y t ∣ x , y < t ) ) 式(1) max_{\Phi}\sum_{(x,y)\in Z}\sum^{|y|}_{t1}log(P_{\Phi}(y_t|x,y<t)) \qquad \text{式(…

Umi-OCR 全家桶

介绍&#xff1a; 下载 访问官网地址 https://github.com/hiroi-sora/umi-ocrhttps://github.com/hiroi-sora/umi-ocr 点击下载&#xff08;.exe 文件 安装即可&#xff09; 桌面使用 安装完毕后去安装路径下点击 Umi-OCR.exe &#xff08;默认不会生成桌面的快捷方式&…

oracle中OS BLOCK的含义

在Oracle数据库中&#xff0c;OS BLOCK&#xff08;操作系统数据块&#xff09;是指操作系统层面上的数据块&#xff0c;它与Oracle数据库内部的逻辑存储单元BLOCK&#xff08;数据块&#xff09;有所区别但密切相关。以下是对OS BLOCK的详细解释&#xff1a; 定义与概念 OS BL…

mac部署GPT-SoVITS,生成粤语踩坑点及使用记录

自己录音&#xff0c;普通话或者粤语 注意&#xff1a; 与在其他设备上训练的模型相比&#xff0c;在mac上使用gpu训练的模型的质量明显较低&#xff0c;因此我们暂时使用cpu代替。 Install Xcode command-line tools by running xcode-select --install.Install FFmpeg by …

STM32-SPI通信外设

目录 一&#xff1a;SPI外设简介 SPI框图​编辑 SPI逻辑 ​编辑 主模式全双工连续传输 ​编辑 非连续传输 二&#xff1a;硬件SPI读写W25Q64 1.接线&#xff1a; 2. 代码 SPI外设的初始化 生成时序 一&#xff1a;SPI外设简介 STM32内部集成了硬件SPI收发电路&#…

游戏引擎学习第158天

回顾和今天的计划 我们在这里会实时编码一个完整的游戏&#xff0c;没有使用引擎或库&#xff0c;一切都由我们自己做所有的编程工作&#xff0c;游戏中的每一部分&#xff0c;无论需要做什么&#xff0c;我们都亲自实现&#xff0c;并展示如何完成这些任务。今天&#xff0c;…

指令微调 (Instruction Tuning) 与 Prompt 工程

引言 预训练语言模型 (PLMs) 在通用语言能力方面展现出强大的潜力。然而&#xff0c;如何有效地引导 PLMs 遵循人类指令&#xff0c; 并输出符合人类意图的响应&#xff0c; 成为释放 PLMs 价值的关键挑战。 指令微调 (Instruction Tuning) 和 Prompt 工程 (Prompt Engineerin…

实验三 Python 数据可视化 Python 聚类-K-means(CQUPT)

一、实验目的 Python 数据可视化&#xff1a; 1、学习使用 jieba、wordcloud 等类库生成词云图。 2、学习使用 Matplotlib 库进行数据可视化。 Python 聚类-K-means&#xff1a; 1、理解聚类非监督学习方法的基本原理。 2、掌握 Python、numpy、pandas、sklearn 实现聚类…

Linux--git

ok&#xff0c;我们今天来学习如何在Linux上建立链接git 版本控制器Git 不知道你⼯作或学习时&#xff0c;有没有遇到这样的情况&#xff1a;我们在编写各种⽂档时&#xff0c;为了防⽌⽂档丢失&#xff0c;更改 失误&#xff0c;失误后能恢复到原来的版本&#xff0c;不得不…