工具类的详解

article/2025/9/27 9:57:06
  • 工具类网站

1、工具类

1.1 定义

工具类是为了提供一些通用的、某一非业务领域内的公共方法,不需要配套的成员变量,仅仅是作为工具方法被使用。所以将它做成静态方法最合适,不需要实例化,能够获取到方法的定义并调用就行。

1.2 不实例化的原因

如果要实例化一个工具类,就需要一定的内存空间,工具类提供的是静态方法,通过类就能调用,所以不必浪费内存去实例化工具类对象。

1.3 做法

Java项目中使用的工具类非常多,比如JDK自己的工具类java.lang.Math、java.util.Collections等都是我们经常用到的,工具类的方法和属性都是静态的,不需要生成实例即可访问,而且JDK也做了很好的处理,由于不希望被初始化,于是就设置构造函数为private访问权限,表示除了类本身外,谁都不能产生一个实例,因此,其他类只能通过类名来访问,不能通过实例对象访问。我们来看一下java.lang.Math代码:

public final class Math {  /**  * Don't let anyone instantiate this class.  */  private Math() {}  
} 

这在平台型或框架型项目中已经足够了。但是如果已经告诉你不能这么做了,你还要生成一个Math实例来访问静态方法和属性(Java的反射是如此的发达,修改个构造函数的访问权限易如反掌),那我就不保证正确性了,隐藏问题随时都有可能爆发!那我们在项目开发中有没有更好的限制办法呢?有,即不仅仅设置成private访问权限,还抛异常,代码如下:

public class UtilsClass {  private UtilsClass(){  throw new Error("不要实例化我!");  }  
} 

如此做才能保证一个工具类不被实例化(private+抛异常),并且保证所有的访问都是通过类名来进行的。需要注意一点的是,此工具类最好不要做继承的打算,因为如果子类可以实例化的话,那就要调用父类的构造函数,可是父类没有可以被访问的构造函数,于是问题就会出现。

总结:在使用私有构造器的基础下,再在构造方法中返回一个异常,因为虽然外部类无法实例化该类,但是内部类可以实例化该类。如果只是通过私有化构造器,那么通过反射的方式,还是可以实例化该类,因此,必须在私有构造器中添加一个异常,这样,当执行构造方法的时候,就会抛出异常,从而停止实例化。


2、例子

2.1 apache common包 CollectionUtils 使用

  • 集合判断: 判断集合是否为空
CollectionUtils.isEmpty(null)				true
CollectionUtils.isEmpty(new ArrayList())	true
CollectionUtils.isEmpty({a,b})				false
  • 判断集合是否不为空
CollectionUtils.isNotEmpty(null)				false
CollectionUtils.isNotEmpty(new ArrayList())		false
CollectionUtils.isNotEmpty({a,b})				true
  • 2个集合间的操作:
    • 集合a: {1,2,3,3,4,5}
    • 集合b: {3,4,4,5,6,7}
CollectionUtils.union(a, b)					(并集): {1,2,3,3,4,4,5,6,7}
CollectionUtils.intersection(a, b)			(交集): {3,4,5}
CollectionUtils.disjunction(a, b)			(交集的补集): {1,2,3,4,6,7}
CollectionUtils.disjunction(b, a)			(交集的补集): {1,2,3,4,6,7}
CollectionUtils.subtract(a, b)				(AB的差): {1,2,3}
CollectionUtils.subtract(b, a)				(BA的差): {4,6,7}

参考:https://blog.csdn.net/weixin_33898233/article/details/86114780
链接:https://blog.csdn.net/shb_derek1/article/details/9624897

3、使用BeanUtils工具类进行对象之间的属性拷贝

使用BeanUtils.copyProperties进行对象之间的属性拷贝

3.1 问题引入

需要将两个不同的对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码,将对象中的属性值从一种类型转换成另一种类型。这种转换最原始的方式就是手动编写大量的 get/set代码,当然这是我们开发过程不愿意去做的,因为它确实显得很繁琐。为了解决这一痛点,就诞生了一些方便的类库,常用的有 apache的 BeanUtils,spring的 BeanUtils这些库可以帮助我们轻松的实现对象属性的拷贝而不需要我们再手动get/set.

3.2 前置知识

如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

3.3 BeanUtils

BeanUtils组件是用于简化javaBean的操作,那么什么是javaBean呢?简单来说,javaBean实质就是java类,只不过是遵循了某种规范的java类。javaBean的特点:

  • 必须具有一个无参的构造方法
  • 属性必须私有化
  • 私有化的属性必须通过public类型的方法来暴露,也就是说要出现setXXX()、getXXX()或者isXXX()的方法

BeanUtils用法

  • 对象的拷贝,BeanUtils.copyProperties(Object dest,Object orig)
  • 对象属性的拷贝,BeanUtils.copyProperties(Object bean,String name,Object value)或BeanUtils.setProperty(Object bean,String name,Object value)
  • map数据封装到Javabean,populate(Object bean, Map<String,? extends Object> properties)

注意事项

  • target的属性必需要有set方法,否则该属性不会被拷贝
  • 使用 org.springframework.beans.BeanUtils 包下的BeanUtils,不要使用apache包下的,apache包下的BeanUtils效率低,已不推荐使用
  • 如果source与target中存在属性名相同类型不同的情况,则target中对应的属性值为null
  • 如果target中不存在source中含有的属性,则丢弃source中该属性的值

3.3.1 Apache 的 BeanUtils

Apache 的 BeanUtils 对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它差劲的性能,在Java开发手册中也提到尽量避免使用Apache 的 BeanUtils 工具类。

Apache里面的BeanUtils组件使用:

package org.BeanUtils.Apache_;import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
// 当Student对象中的属性没有get/set方法时,拷贝后属性值为null   !!!
public class Student {private String name;private String id;private int age;private String sex;private Date d;public Student() {super();}public Date getD() {return d;}public void setD(Date d) {this.d = d;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getId() {return id;}public void setId(String id) {this.id = id;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}@Overridepublic String toString() {return "Student [name=" + name + ", id=" + id + ", age=" + age+ ", sex=" + sex + ", d=" + d + "]";}public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {/*** 1.实现对象的属性拷贝, 对于基本数据类型,会自动进行类型转换* BeanUtils.copyProperties(Object bean,String name,Object value)或者BeanUtils.setProperty(Object bean,String name,Object value)*  - bean:javaBean对象*  - name:对象的属性名称*  - value:对象的属性值*/Student s = new Student();/*一般的操作s.setId("1221212");s.setName("老王");System.out.println(s);*/BeanUtils.copyProperty(s, "id", "2018100712");System.out.println(s);/*** 实现对象之间的拷贝:Object dest<---Object orig*  - BeanUtils.copyProperties(Object dest,Object orig)*/Student s2 = new Student();BeanUtils.copyProperties(s2, s);System.out.println(s2);/*** map数据封装到Javabean*  - populate(Object bean, Map<String,? extends Object> properties)*  - 要map中的数据封装到JavaBean中去,需要map中的key与JavaBean里面的私有化的属性要相匹配*///创建对象Student s3 = new Student();//1.map的数据拷贝到对象中去Map<String, Object> map = new HashMap<String, Object>();map.put("id", "12233");map.put("name", "老王");map.put("sex", "男");BeanUtils.populate(s3, map);System.out.println(s3);System.out.println("-----------------------------------------------------------------------");/*** 类型转换器*  - 当javaBean中出现非基本类型数据的私有化属性,并且需要对该数据进行封装时,就要去注册该数据类型的类型转换器了,不然就会出现错误,比如该Student对象中的日期类型。*  - 使用工具类提供的类型转换器,需要注意的是,当日期字符串是空字符串或者存在空格的时候,会报错!*/String name = "老王";String id = "121212";//当日期字符串出现空字符串或者出现空格的情况,会报错org.apache.commons.beanutils.ConversionExceptionString date = "2018-09-12";//利用组件提供的日期类型转换器,提供一个实现类ConvertUtils.register(new DateLocaleConverter(), Date.class);//把数据封装到对象中去Student student = new Student();BeanUtils.copyProperty(student, "name", name);BeanUtils.copyProperty(student, "id", id);BeanUtils.copyProperty(student, "d", date);System.out.println(student);}
}

在这里插入图片描述

3.3.2 Spring 的 BeanUtils

Spring的BeanUtils也是使用 copyProperties方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。

  • PersonDest.java
package org.BeanUtils.Spring_;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonDest {private int id;private String username;private Integer age;@Overridepublic String toString() {return "PersonDest{" +"id=" + id +", username='" + username + '\'' +", age=" + age +'}';}
}
  • PersonSource.java
package org.BeanUtils.Spring_;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonSource {private int id;private String username;private String password;private Integer age;@Overridepublic String toString() {return "PersonSource{" +"id=" + id +", username='" + username + '\'' +", password='" + password + '\'' +", age=" + age +'}';}
}
  • PersonSpringBeanUtils.java
package org.BeanUtils.Spring_;import org.springframework.beans.BeanUtils;import java.lang.reflect.InvocationTargetException;/*
https://bbs.csdn.net/topics/360115761
PersonSource类,PersonDest类,单独生成一个public class,这种bean文件最好是单独一个class
// The class must be public, and provide a public constructor that accepts no arguments. This allows tools and applications to dynamically
// create new instances of your bean, without necessarily knowing what Java class name will be used ahead of time.
class PersonSource{}
class PersonDest{}
*/
public class PersonSpringBeanUtils {public static void main(String[] args) throws InvocationTargetException, IllegalAccessException { //下面只是用于单独测试PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);PersonDest personDest = new PersonDest();// 如果source与target中存在属性名相同类型不同的情况,则target中对应的属性值为nullBeanUtils.copyProperties(personSource, personDest);System.out.println("persondest: " + personDest);}
}

3.3.3 总结

  • 以上简要的分析两种BeanUtils,因为Apache下的BeanUtils性能较差,不建议使用,可以使用 Spring的BeanUtils,
  • 区别
介绍spring的BeanUtilscommons的BeanUtils
方法copyProperty和copyPropertiescopyProperties
参数src ,destdest,src

https://blog.csdn.net/weixin_36605200/article/details/83009443

3.4 BeanUtils是浅拷贝

  • 链接
  • 浅谈BeanUtils的拷贝,深度克隆

结论:

  • 1、BeanUtils的copyProperties()方法并不是完全的深度克隆,在包含有引用类型的对象拷贝上就可能会出现引用对象指向同一个的情况,且该方法的性能低下,项目中一定要谨慎使用。
  • 2、要实现高性能且安全的深度克隆方法还是实现Serializable接口,多层克隆时,引用类型均要实现Serializable接口。
  • YTTLJ_Father.java
package org.BeanUtils.Spring_;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class YTTLJ_Father {// 长相private String face;// 身高private String height;// 生命private YTTLJ_Life life;
}
  • YTTLJ_Life.java
package org.BeanUtils.Spring_;import lombok.Data;@Data
public class YTTLJ_Life {private String status;
}
  • YTYLJ_Son.java
package org.BeanUtils.Spring_;import com.alibaba.fastjson.JSON;
import lombok.Data;
import org.springframework.beans.BeanUtils;/*** https://blog.csdn.net/enthan809882/article/details/104956537** - BeanUtils还是浅拷贝* - 什么情况适合用BeanUtils*      - 如果都是单一的属性,那么不涉及到深拷贝的问题,适合用BeanUtils。* - 有子对象就一定不能用BeanUtils么,并不绝对,这个要区分考虑:*      - 子对象还要改动。*      - 子对象不怎么改动。*  虽然有子对象,但是子对象并不怎么改动,那么用BeanUtils也是没问题的。**  - 结论:BeanUtils的copyProperties()方法并不是完全的深度克隆,在包含有引用类型的对象拷贝上就可能会出现引用对象指向同一个的情况,且该方法的性能低下,项目中一定要谨慎使用。*  - 要实现高性能且安全的深度克隆方法还是实现Serializable接口,多层克隆时,引用类型均要实现Serializable接口。*/
@Data
public class YTYLJ_Son extends YTTLJ_Father {private YTTLJ_Life life;public static void main(String[] args) {/*** 第一集:张翠山在荒岛上快乐地生活*/YTTLJ_Father cuishan = new YTTLJ_Father();cuishan.setFace("handsome");cuishan.setHeight("180");YTTLJ_Life cuishanLife = new YTTLJ_Life();cuishanLife.setStatus("alive");cuishan.setLife(cuishanLife);/*** 第二集:生了个孩子叫张无忌*/YTYLJ_Son wuji=new YTYLJ_Son();BeanUtils.copyProperties(cuishan, wuji);System.out.println();System.out.println(JSON.toJSONString(cuishan));System.out.println(JSON.toJSONString(wuji));System.out.println("-----------------------------------------------------");/*** 第三集:武当山被各大门派逼迫而自刎* 受浅拷贝的影响: 翠山自刎,无忌也挂了*/cuishanLife.setStatus("dead");          // 翠山自刎,无忌也被连带自刎了System.out.println();System.out.println(JSON.toJSONString(cuishan));System.out.println(JSON.toJSONString(wuji));System.out.println("-----------------------------------------------------");System.out.println("受浅拷贝的影响: 翠山自刎,无忌也挂了");System.out.println("-----------------------------------------------------");/*** 第三集:武当山被各大门派逼迫而自刎* 受浅拷贝的影响: 翠山自刎,无忌也挂了,这样的话,电视剧就结束了,因此,将无忌置为alive,但是翠山也活了*/cuishanLife.setStatus("dead");          // 翠山自刎,无忌也被连带自刎了YTTLJ_Life wujiLife = wuji.getLife();wujiLife.setStatus("alive");            // 将无忌复活,翠山被连带复活了wuji.setLife(wujiLife);System.out.println();System.out.println(JSON.toJSONString(cuishan));System.out.println(JSON.toJSONString(wuji));System.out.println("-----------------------------------------------------");System.out.println("受浅拷贝的影响: 翠山自刎,无忌也挂了,这样的话,电视剧就结束了,因此,将无忌置为alive,但是翠山也活了");System.out.println("-----------------------------------------------------");/*** 第三集:武当山被各大门派逼迫而自刎* 翠山和无忌互不影响*/cuishanLife.setStatus("dead"); // 翠山自刎了  该行放在上下均可// 无忌用个新对象 不受翠山影响了YTTLJ_Life wujiLife_new = new YTTLJ_Life();wujiLife_new.setStatus("alive");wuji.setLife(wujiLife_new);System.out.println();System.out.println(JSON.toJSONString(cuishan));System.out.println(JSON.toJSONString(wuji));System.out.println("-----------------------------------------------------");System.out.println(" 翠山和无忌互不影响 ");System.out.println("-----------------------------------------------------");}
}

http://chatgpt.dhexx.cn/article/O0X24Z47.shtml

相关文章

学习日记-安卓Package Manager和Package Installer

安装和卸载APK&#xff08;安卓应用程序包文件&#xff09;&#xff0c;运作原理。 什么是Package Manager&#xff08;包管理器&#xff09;和Package Installer&#xff08;程序安装包&#xff09;&#xff1f; APK文件保存在Android的哪个地方&#xff1f; APK文件安装过…

PackageInstaller (tv 修改安装app界面按钮及自动获取焦点)附源码分析

\packages\apps\PackageInstaller 一、一条真实的修改记录 TVOS基于的是一套板卡厂商原有的源码(mstar android8.0版本&#xff09;原生的这个app安装界面&#xff0c;存在俩个比较严重的用户体验问题&#xff0c; 1’、下面那俩按钮太小了&#xff0c;而且…

android packages/apps 加入工程,深入安卓Package Manager和Package Installer

我们每天都在安装和卸载APK(安卓应用程序包文件)&#xff0c;或许一天会有好几次&#xff0c;但是你有想过下面问题吗&#xff1f;什么是Package Manager(包管理器)和Package Installer(程序安装包)&#xff1f; APK文件保存在Android的哪个地方&#xff1f; APK文件安装过程的…

RK3568平台开发系列讲解(安卓篇)PackageInstaller(应用安装)流程介绍

文章目录 <font color=#0990d9>一、PackageInstaller入口<font color=#0990d9>二、InstallStart<font color=#0990d9>三、InstallStaging<font color=#0990d9>四、PackageInstallerActivity<font color=#0990d9>五、InstallInstalling<font c…

Android9.0 PM机制系列(一)PackageInstaller初始化解析

前言 包管理机制是Android中的重要机制&#xff0c;是应用开发和系统开发需要掌握的知识点之一。 包指的是Apk、jar和so文件等等&#xff0c;它们被加载到Android内存中&#xff0c;由一个包转变成可执行的代码&#xff0c;这就需要一个机制来进行包的加载、解析、管理等操作&…

PackageInstaller源码分析(一)

本篇博客分析PackageInstaller源码目的是分析Android权限机制&#xff0c;Android App的权限在应用被安装时&#xff0c;用户选择授予或者拒绝。所以&#xff0c;分析Android权限机制源码的第一步分析应用程序安装时的行为。   此次阅读源码旨在解决的问题&#xff1a;Andro…

A*B problem(FFT)

A*B problem&#xff08;FFT&#xff09; 设两个多项式\(A(x)\)和\(B(x)\)&#xff0c;它们的系数镜像反转一下&#xff0c;得到的多项式是\(A(x)\)和\(B(x)\)。那么\(C(x)A(x)*B(x)\)和\(C(x)A(x)*B(x)\)的系数也是镜像反转的。这个&#xff0c;&#xff0c;感性理解一下吧。 …

【kissfft】使用过程中的一些坑总结

API kissfft有两套API&#xff0c; 一个是在kiss_fftr.h中 另一个在kiss_fft.h中 区别 Basic API还是kiss_fft.h里的&#xff0c;kiss_fftr.h是在kiss_fft.h的基础上封装了一层。 Basic API只有fft没有见到ifft&#xff1f;&#xff1f; 利用频域数据的共轭对称性可以使用…

2020山东大学计算机组成原理课程设计报告

《计算机组成原理》 课程设计报告 微指令模型机实现 班级&#xff1a; 姓名&#xff1a; 学号&#xff1a; 小组成员&#xff1a; 完成日期&#xff1a;2020.10.16 一、计算机的功能和用途 通过该课程设计的学习&#xff0c;我们设计一台模型机&#xff0c;该模型机运行…

创建react应用程序_创建多版本React应用程序的6个步骤

创建react应用程序 The React team said that there are no new features in React 17, but react17.0.0-rc.0 comes with the power to lazy load and deep integrate multiple versions of React. This no-feature is larger than any feature, which is a stepping stone fo…

你真的懂package.json吗

点击蓝字 「前端小苑」关注我 作者 | MasonEast 编辑 | 桔子酱 前言 在Node.js中&#xff0c;模块是一个库或框架&#xff0c;也是一个Node.js项目。Node.js项目遵循模块化的架构&#xff0c;当我们创建了一个Node.js项目&#xff0c;意味着创建了一个模块&#xff0c;这个模块…

《Linux编程》上机作业 ·004【文件I/O操作】

注&#xff1a;前言、目录见 https://blog.csdn.net/qq_44220418/article/details/108428971 友情提醒&#xff1a;仅供参考理解&#xff0c;请勿直接复制粘贴 友情提醒&#xff1a;仅供参考理解&#xff0c;请勿直接复制粘贴 友情提醒&#xff1a;仅供参考理解&#xff0c;…

CPU比GPU训练神经网络快十几倍,英特尔:别用矩阵运算了

来源丨机器之心 神经网络训练通常是 GPU 大显身手的领域&#xff0c;然而莱斯大学和英特尔等机构对 GPU 的地位发起了挑战。 在深度学习与神经网络领域&#xff0c;研究人员通常离不开 GPU。得益于 GPU 极高内存带宽和较多核心数&#xff0c;研究人员可以更快地获得模型训练的结…

用于基于 CNT 的射频辐射热计开发研究的 CPX-VF 探针台

我们会不时强调我们的低温探针台如何用于有趣的研究。我们最新的应用重点是阿克伦大学领导的工作&#xff0c;并发表在上个月的IEEE 微波理论与技术汇刊上。与来自美国陆军和 Nano-C Inc.&#xff08;马萨诸塞州 Westwood 的纳米结构碳材料及其应用开发商&#xff09;的研究人员…

ProJet 3510 CPX蜡模3D打印机在珠宝行业成功应用

传统的首饰设计是一个细致和增量的过程。传统设计从设计师的构图开始&#xff0c;一旦草图被批准后,就会雕刻成模型&#xff0c;如果蜡模没有足够接近原始草图或未能满足客户的期望&#xff0c;必须重做,这样会浪费大量的时间。使用ProJet 3510 CPX专业蜡成型3 d打印机&#xf…

基于 CNT 的射频辐射热计开发研究的 CPX-VF 低温探针台

有时&#xff0c;我们喜欢强调我们的低温探针台如何用于有趣的研究。我们最新的应用重点是由阿克伦大学领导并发表在上个月的IEEE Transactions on Microwave Theory and Techniques 上的工作。UA 的 ZEN-Lab 的Michael Gasper 和 Ryan Toonen 博士与美国陆军和 Nano-C Inc.&am…

Parker驱动器维修COMPAX控制器维修CPX0200H

COMPAX控制器&#xff1a;由不同的模拟功率控制信号&#xff0c;由MOSFET IC级驱动器GND/PGND&#xff08;功率接地&#xff09;&#xff09;的信号控制&#xff0c;则应分别接地。使用IC的小信号部分的控制IC&#xff0c;SGND信号与功率地之间的连接点。合理的方法是地信号地返…

用于 CPX、CPX-VF 和 CRX-VF 探针台的新手提箱选项

如果您正在寻找一种简单的方法来将样品从手套箱、干燥箱或其他惰性气氛容器转移到高真空、低温探测环境&#xff0c;您可能会感兴趣&#xff1a;一个新的专用手提箱 (PS-SC- CPX) 与可安装在我们的CPX、CPX-VF或CRX-VF探针台上的负载锁定组件 (PS-LL-CPX) 一起使用。 该手提箱具…

GE IC697CPX935 CPU模块PDF帅

IC697CPX935 是 GE 自动化和控制公司制造的具有三个内置串行端口的单槽 PLC CPU。它能够对系统进行实时控制。使用 VMEC.1 格式&#xff0c;IC697CPX935 可以通过安装在机架上的背板与不同的“智能选项”模块进行通信。该设备通过三位运行/停止控制开关或连接到运行适当软件的计…

micropython仿真器_microbit/cpx 的 python模拟器:Device Simulator Express

Device Simulator Express是一个 VSCode 的编程扩展,使用它无需硬件就能对 Circuit Playground Express(CPX)或 BBC micro:bit 仿真和调试python程序,此外还可以通过串口观察设备的输出。Device Simulator Express 和 makecode 中的设备模拟器功能类似,但它是一个 python 程…