zoukankan      html  css  js  c++  java
  • 搭建基于hyperledger fabric的联盟社区(四) --chaincode开发

    前几章已经分别把三台虚拟机环境和配置文件准备好了,在启动fabric网络之前我们要准备好写好的chaincode。chaincode的开发一般是使用GO或者JAVA,而我选择的是GO语言。先分析一下官方最典型的一个chaincode--fabcar,然后着重介绍一下shim.ChaincodeSubInterface,最后在贴上我自己的chaincode。

    一.chaincode主要框架结构

    1.1 引入了4个程序库,用于格式化、处理字节、读取和写入JSON,以及字符串操作。2个Hyperledger Fabric 特定的智能合同库。shim包提供了一些 API,以便chaincode与底层区块链网络交互来访问状态变量、交易上下文、调用方证书和属性,并调用其他chaincode和执行其他操作。

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"strconv"
    
    	"github.com/hyperledger/fabric/core/chaincode/shim"
    	sc "github.com/hyperledger/fabric/protos/peer"
    )
    

    1.2 定义SmartContract结构体,然后在该struct上定义Init和Invoke两个函数

    type SmartContract struct {
    }
    

    1.3 定义具有四个变量的汽车结构体,结构体标签被编码/json库使用。

    type Car struct {
    	Make   string `json:"make"`
    	Model  string `json:"model"`
    	Colour string `json:"colour"`
    	Owner  string `json:"owner"`
    }
    

    1.4 当智能合同“fabcar”由区块链网络实例化时,Init方法被调用。在Go中通过给函数标明所属类型,来给该类型定义方法,下面的 s *SmartContract即表示给SmartContract声明了一个方法。在Init和Invoke的时候,都会传入参数stub shim.ChaincodeStubInterface,这个参数提供的接口为我们编写ChainCode的业务逻辑提供了大量的实用方法。假设一切顺利,将返回一个表示初始化已经成功的sc.Response对象。

    func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
    	return shim.Success(nil)
    }
    

    1.5 应用程序请求运行智能合约fabcar后,Invoke方法被调用。在Invoke的时候,由传入的参数来决定我们具体调用了哪个方法,所以需要用GetFunctionAndParameters解析调用的时候传入的参数。

    func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
    
    	// 检索请求的智能合约的函数和参数,GetFunctionAndParameters() (string, []string)将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter
    	function, args := APIstub.GetFunctionAndParameters()
    	// 找到合适的处理函数以便与账本进行交互
    	if function == "queryCar" {
    		return s.queryCar(APIstub, args)
    	} else if function == "initLedger" {
    		return s.initLedger(APIstub)
    	} else if function == "createCar" {
    		return s.createCar(APIstub, args)
    	} else if function == "queryAllCars" {
    		return s.queryAllCars(APIstub)
    	} else if function == "changeCarOwner" {
    		return s.changeCarOwner(APIstub, args)
    	}
    
    	return shim.Error("Invalid Smart Contract function name.")
    }
    

    1.6 主函数仅与单元测试模式相关,任何GO程序的起点都是main函数,在这里只是为了完整性。

    func main() {
    
    	// 创建了新的智能合同,并向对等节点注册它
    	err := shim.Start(new(SmartContract))
    	if err != nil {
    		fmt.Printf("Error creating new Smart Contract: %s", err)
    	}
    }
    

    二.对State DB (状态数据库)的增删改查 

    2.1 查询某辆汽车

    通过GetState(key string) ([]byte, error)查询数据。因为我们是Key Value数据库,所以根据Key来对数据库进行查询,是一件很常见,很高效的操作,返回的数据是byte数组。

    func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 1 {
    		return shim.Error("Incorrect number of arguments. Expecting 1")
    	}
    
    	carAsBytes, _ := APIstub.GetState(args[0])
    	return shim.Success(carAsBytes)
    }
    

    2.2 初始化账本

    通过PutState(key string, value []byte) error 增改数据,对于State DB来说,增加和修改数据是统一的操作,因为State DB是一个Key Value数据库,如果我们指定的Key在数据库中已经存在,那么就是修改操作,如果Key不存在,那么就是插入操作。对于实际的系统来说,我们的Key可能是单据编号,或者系统分配的自增ID+实体类型作为前缀,而Value则是一个对象经过JSON序列号后的字符串。

    func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
    	cars := []Car{
    		Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
    		Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
    		Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
    		Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
    		Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
    		Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
    		Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
    		Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
    		Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
    		Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
    	}
    
    	i := 0
    	for i < len(cars) {
    		fmt.Println("i is ", i)
    		carAsBytes, _ := json.Marshal(cars[i])
    		APIstub.PutState("CAR"+strconv.Itoa(i), carAsBytes)
    		fmt.Println("Added", cars[i])
    		i = i + 1
    	}
    
    	return shim.Success(nil)
    }
    

    2.3 创建汽车

    把对象转换为JSON的方法(函数)为 json.Marshal(),也就是说,这个函数接收任意类型的数据 v,并转换为字节数组类型,返回值就是我们想要的JSON数据和一个错误代码。当转换成功的时候,这个错误代码为nil。

    func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 5 {
    		return shim.Error("Incorrect number of arguments. Expecting 5")
    	}
    
    	var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}
    
    	carAsBytes, _ := json.Marshal(car)
    	APIstub.PutState(args[0], carAsBytes)
    
    	return shim.Success(nil)
    }
    

    2.4 查询所有汽车

    Key区间查询GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) 提供了对某个区间的Key进行查询的接口,适用于任何State DB。由于返回的是一个StateQueryIteratorInterface(迭代器)接口,我们需要通过这个接口再做一个for循环,才能读取返回的信息,所有我们可以独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。

    func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
    
    	startKey := "CAR0"
    	endKey := "CAR999"
    
    	resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
    	if err != nil {
    		return shim.Error(err.Error())
    	}
       //defer关键字用来标记最后执行的Go语句,一般用在资源释放、关闭连接等操作,会在函数关闭前调用。多个defer的定义与执行类似于栈的操作:先进后出,最先定义的最后执行。
    	defer resultsIterator.Close()
    
    	// buffer 是一个包含查询结果的JSON数组,bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte,这样直接定义一个 Buffer 变量,而不用初始化。
    	var buffer bytes.Buffer
    	buffer.WriteString("["]
    
    	bArrayMemberAlreadyWritten := false
       //迭代器的两个方法,hasNext:没有指针下移操作,只是判断是否存在下一个元素。next:指针下移,返回该指针所指向的元素。
    	for resultsIterator.HasNext() {
    		queryResponse, err := resultsIterator.Next()
    		if err != nil {
    			return shim.Error(err.Error())
    		}
    		// 在数组成员前加一个逗号
    		if bArrayMemberAlreadyWritten == true {
    			buffer.WriteString(",")
    		}
    		buffer.WriteString("{"Key":")
    		buffer.WriteString(""")
    		buffer.WriteString(queryResponse.Key)
    		buffer.WriteString(""")
    
    		buffer.WriteString(", "Record":")
    		// Record 是一个JSON对象,所以我们按原样写
    		buffer.WriteString(string(queryResponse.Value))
    		buffer.WriteString("}")
    		bArrayMemberAlreadyWritten = true
    	}
    	buffer.WriteString("]")
    
    	fmt.Printf("- queryAllCars:
    %s
    ", buffer.String())
    
    	return shim.Success(buffer.Bytes())
    }
    

    2.5 改变汽车拥有人

    Unmarshal是用于反序列化json的函数根据data将数据反序列化到传入的对象中。

    func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 2 {
    		return shim.Error("Incorrect number of arguments. Expecting 2")
    	}
    
    	carAsBytes, _ := APIstub.GetState(args[0])
    	car := Car{}
    
    	json.Unmarshal(carAsBytes, &car)
    	car.Owner = args[1]
    
    	carAsBytes, _ = json.Marshal(car)
    	APIstub.PutState(args[0], carAsBytes)
    
    	return shim.Success(nil)
    }
    

    以上就是官方链代码fabcar的分析  

    三.链码开发API

    作为chaincode中的利器shim.ChaincodeSubInterface提供了一系列API供开发者在编写链码时灵活选择使用。

    这些API可分为四类:账本状态交互API、交易信息相关API、参数读取API、其他API,下面分别介绍。

    3.1账本状态交互API

    chaincode需要将一些数据记录在分布式账本中。需要记录的数据称为状态state,以键值对(key-value)的形式存储。账本状态API可以对账本状态进行操作,十分重要。方法的调用会更新交易提案的读写集和,在committer进行验证时会在此执行,跟账本状态进行比对,这类API的大致功能如下:

    3.2交易信息相关API

    交易信息相关API可以获取到与交易信息自身相关的数据。用户对链码的调用(初始化和升级时调用Init()方法,运行时调用Invoke()方法)过程中会产生交易提案。这些API支持查询当前交易提案的一些属性,具体信息如下:

    3.3 参数读取API

    调用链码时支持传入若干参数,参数可通过API读取。具体信息如下:

    3.4其他API

    除了上面一些API以外还有一些辅助API,如下:

    四. 社区联盟chaincode

    4.1 业务功能需求

    4.1.1 AddPost(Post)

    本函数功能为存入一个帖子。

    参数: 

    Post 为JSON 格式数据,包含帖子的的基本信息。须将字段名作为Key,字段的值作为 Value。

    返回值:

    -1:操作不成功

    某个正整数:操作成功,返回值表示新帖的ID

    4.1.2 UpdatePost(Post)

    本函数功能为更改一个已经存在的帖子。

    参数: 

    Post 为JSON 格式数据,包含帖子的的基本信息,其中帖子ID不可为空。须将字段名作为Key,字段的值作为 Value。

    返回值:

    -1:操作不成功

    1:操作成功

    4.1.3 RichQueryPost (Attribute, Operator, Value)

    本函数功能为查找帖子。

    参数: 

    Attribute 表示帖子的属性名称,须符合附件一中的字段名。

    Operator 表示比较运算符的代码。如下所示 

    0: =

    1: >

    2: >=

    3: <

    4: <=

    5: between

    6: like

    Value 为属性的值。若Operator 为0(等于),则本函数将返回对应属性中等于指定Value的所有帖子;若Operator为1~4,则Value 视为单个值(单个数值或单个字符串);若Operator为5,则Value须为两个值,中间以逗号分开;若Operator为6,则本函数将返回对应属性中包含指定Value的所有帖子。

    返回值:

    -1:操作不成功

    JSON格式字符串:操作成功,所有符合条件的帖子将组织为JSON格式的数组,数组中每个元素为一个帖子。

    注:目前仅支持单属性的查找。

    4.1.4 GetPostNum(Attribute, Operator, Value)

    本函数功能为查找返回某个条件的帖子的数量。

    参数的含义与函数QueryPost相同。

    返回值:

    -1:操作不成功

    某个正整数:操作成功,返回值符合查询条件的帖子的数量

     

     

     

    4.2 编写chaincode遇到的问题

    在实现有transaction功能的函数时,在函数里面写一个返回值,并不能像查询类型的函数一样在用fabric SDK调用时得到对应的返回值。addpost功能需要返回新帖子的ID,但是我的帖子id正好就是键值对的数量,我通过fabric node SDK 返回了键值对的数量,也就是返回了新帖子的ID。

    RichQueryPost和GetPostNum两个函数都会用到富查询。富查询的语法可以参考couchdb官方文档关于Selector语法部分的介绍:

    http://docs.couchdb.org/en/2.0.0/api/database/find.html#find-selectors

    https://github.com/cloudant/mango

    但是模糊查询功能暂时没有实现。(下个版本会实现)

    4.3 chaincode代码

    我们将自己编写符合业务逻辑的chaincode放在peer0.org1和peer0.org2的/go/src/github.com/hyperledger/fabric/examples/chaincode/go/community

    目录下,之后启动docker容器的时候会自动挂载chaincode

    cd ~/go/src/github.com/hyperledger/fabric/examples/chaincode/go/
    mkdir community

    完整chaincode代码如下

    package main
    
    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"strconv"
    	"strings"
        
    	"github.com/hyperledger/fabric/core/chaincode/shim"
    	sc "github.com/hyperledger/fabric/protos/peer"
    )
    
    type SmartContract struct {
    }
    
    type Post struct {
    	Id   string `json:"id"`
    	OriginalWebsite  string `json:"originalwebsite"`
    	OriginalID string `json:"originalid"`
    	Title  string `json:"title"`
    	Content  string `json:"content"`
    	AuthorId  string `json:"authorid"`
    	PublishTime  string `json:"publishtime"`
    	UpdateTime  string `json:"updatetime"`
    	Category  string `json:"category"`
    	SourceId  string `json:"sourceid"`
    	Labels  string `json:"labels"`
    	Follower_num  int `json:"follower_num"`
    	Browse_num  int `json:"browse_num"`
    	Star_num  int `json:"star_num"`
    } 
    
    type PostLength struct {
        Length int `json:"length"`
    }
    
    
    func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
    	return shim.Success(nil)
    }
    
    
    func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
    	
    	function, args := APIstub.GetFunctionAndParameters()
    	
    	if function == "queryPost" {
    		return s.queryPost(APIstub, args)
    	} else if function == "initLedger" {
    		return s.initLedger(APIstub)
    	} else if function == "addPost" {
    		return s.addPost(APIstub, args)
    	} else if function == "updatePost" {
    		return s.updatePost(APIstub, args)
    	} else if function == "richQueryPosts" {
    		return s.richQueryPosts(APIstub, args)
    	} else if function == "getPostNum" {
    		return s.getPostNum(APIstub, args)
    	} 
    	return shim.Error("Invalid Smart Contract function name.")
    }
    
    func (s *SmartContract) queryPost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 1 {
    		return shim.Error("Incorrect number of arguments. Expecting 1")
    	}
    	postAsBytes, _ := APIstub.GetState(args[0])
    	return shim.Success(postAsBytes)
    }
    
    func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
    	posts := []Post{
    		Post{Id: "1", OriginalWebsite: "b", OriginalID: "c", Title: "d",Content:"e",AuthorId:"f",PublishTime:"g",UpdateTime:"h",Category:"i",SourceId:"j",Labels:"k",Follower_num:100,Browse_num:200,Star_num:300},
    		Post{Id: "2", OriginalWebsite: "bb", OriginalID: "bb", Title: "dd",Content:"ee",AuthorId:"ff",PublishTime:"gg",UpdateTime:"hh",Category:"ii",SourceId:"jj",Labels:"kk",Follower_num:400,Browse_num:500,Star_num:600},	
    	}
    	length := PostLength{Length:len(posts)}
    	lengthAsBytes,_ := json.Marshal(length)
    	APIstub.PutState("POSTLENGTH",lengthAsBytes)
    
    	i := 0
    	for i < len(posts) {
    		fmt.Println("i is ", i)
    		postAsBytes, _ := json.Marshal(posts[i])
    		APIstub.PutState("POST"+strconv.Itoa(i), postAsBytes)
    		fmt.Println("Added", posts[i])
    		i = i + 1
    	}
    
    	return shim.Success(nil)
    }
    
    func (s *SmartContract) addPost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 13 {
    		return shim.Error("Incorrect number of arguments. Expecting 13")
    	} 
    	args10,error := strconv.Atoi(args[10])
    	args11,error := strconv.Atoi(args[11])
    	args12,error := strconv.Atoi(args[12])
    
        if error != nil{
         fmt.Println("String conversion integer failed!")
        }
        lengthAsBytes, _ := APIstub.GetState("POSTLENGTH")
        length := PostLength{}
        json.Unmarshal(lengthAsBytes,&length)
        newlength := length.Length+1   
    	var post = Post{Id: strconv.Itoa(newlength), OriginalWebsite: args[0], OriginalID: args[1], Title: args[2],Content:args[3],AuthorId:args[4],PublishTime:args[5],UpdateTime:args[6],Category:args[7],SourceId:args[8],Labels:args[9],Follower_num:args10,Browse_num:args11,Star_num:args12}
    	postAsBytes, _ := json.Marshal(post)
    	APIstub.PutState("POST"+strconv.Itoa(newlength), postAsBytes)
    	length.Length = newlength
    	lengthAsBytes,_ = json.Marshal(length)
    	APIstub.PutState("POSTLENGTH",lengthAsBytes)
    	return shim.Success(lengthAsBytes)
    }
    
    
    func (s *SmartContract) updatePost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 14 {
    		return shim.Error("Incorrect number of arguments. Expecting 14")
    	}
    	args11,error := strconv.Atoi(args[11])
    	args12,error := strconv.Atoi(args[12])
    	args13,error := strconv.Atoi(args[13])
    	if error != nil{
         fmt.Println("String conversion integer failed!")
        }
    	var post = Post{Id: args[0], OriginalWebsite: args[1], OriginalID: args[2], Title: args[3],Content:args[4],AuthorId:args[5],PublishTime:args[6],UpdateTime:args[7],Category:args[8],SourceId:args[9],Labels:args[10],Follower_num:args11,Browse_num:args12,Star_num:args13}
    	postAsBytes, _ := json.Marshal(post)
    	APIstub.PutState("POST"+args[0], postAsBytes)
    	return shim.Success(nil)
    }
    
    
    func (s *SmartContract) richQueryPosts(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
    
    	if len(args) != 3 {
    		return shim.Error("Incorrect number of arguments. Expecting 3")
    	}
    
    	var queryString string
        
        if args[1] == "0" {
    		queryString = fmt.Sprintf("{"selector":{"%s":"%s"}}", args[0],args[2])
    	} else if args[1] == "1" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$gt":%s}}}", args[0],args[2])
    	} else if args[1] == "2" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$gte":%s}}}", args[0],args[2])
    	} else if args[1] == "3" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$lt":%s}}}", args[0],args[2])
    	} else if args[1] == "4" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$lte":%s}}}", args[0],args[2])
    	} else if args[1] == "5" {
    	  between := strings.Split(args[2], ",")
    	  queryString = fmt.Sprintf("{"selector":{"$and":[{"%s":{"$gte":%s}},{"%s":{"$lte":%s}}]}}", args[0],between[0],args[0],between[1])
    	}  else {
    		return shim.Error("Incorrect number of arguments. Expecting 0~5")
    	}
    
    	resultsIterator, err := APIstub.GetQueryResult(queryString)
    	if err != nil {
    		return shim.Error(err.Error())
    	}
    	defer resultsIterator.Close()
    
    	var buffer bytes.Buffer
    	buffer.WriteString("[")
    
    	bArrayMemberAlreadyWritten := false
    	for resultsIterator.HasNext() {
    		queryResponse, err := resultsIterator.Next()
    		if err != nil {
    			return shim.Error(err.Error())
    		}
    		
    		if bArrayMemberAlreadyWritten == true {
    			buffer.WriteString(",")
    		}
    		buffer.WriteString("{"Key":")
    		buffer.WriteString(""")
    		buffer.WriteString(queryResponse.Key)
    		buffer.WriteString(""")
    
    		buffer.WriteString(", "Record":")
    		
    		buffer.WriteString(string(queryResponse.Value))
    		buffer.WriteString("}")
    		bArrayMemberAlreadyWritten = true
    	}
    	buffer.WriteString("]")
    
    	fmt.Printf("- richQueryPosts:
    %s
    ", buffer.String())
    
    	return shim.Success(buffer.Bytes())
    }
    
    
    func  (s *SmartContract) getPostNum(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
         
        if len(args) != 3 {
    	    return shim.Error("Incorrect number of arguments. Expecting 3")
    	}
       var queryString string
        
        if args[1] == "0" {
    		queryString = fmt.Sprintf("{"selector":{"%s":"%s"}}", args[0],args[2])
    	} else if args[1] == "1" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$gt":%s}}}", args[0],args[2])
    	} else if args[1] == "2" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$gte":%s}}}", args[0],args[2])
    	} else if args[1] == "3" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$lt":%s}}}", args[0],args[2])
    	} else if args[1] == "4" {
    	  queryString = fmt.Sprintf("{"selector":{"%s":{"$lte":%s}}}", args[0],args[2])
    	} else if args[1] == "5" {
    	  between := strings.Split(args[2], ",")
    	  queryString = fmt.Sprintf("{"selector":{"$and":[{"%s":{"$gte":%s}},{"%s":{"$lte":%s}}]}}", args[0],between[0],args[0],between[1])
    	}  else {
    		return shim.Error("Incorrect number of arguments. Expecting 0~5")
    	}
    
    	resultsIterator, err := APIstub.GetQueryResult(queryString)
    	if err != nil {
    		return shim.Error(err.Error())
    	}
    	defer resultsIterator.Close()
    
    	i := 0
    
    	for resultsIterator.HasNext() {
    	  resultsIterator.Next()
    		
    	  i = i + 1
    		
    	}
    	
    
    	fmt.Printf("- getPostNum:
    %s
    ", strconv.Itoa(i))
    
    	return shim.Success([]byte(strconv.Itoa(i)))
    }
    
    
    func main() {
    	err := shim.Start(new(SmartContract))
    	if err != nil {
    		fmt.Printf("Error creating new Smart Contract: %s", err)
    	}
    }
    

      

      

  • 相关阅读:
    ajax标准写法
    javascript序列化表单追加参数
    javascript获取url参数的方式
    jmeter实现跨线程组传递参数
    代码扫描工具SonarQube+Scanner的基本安装与应用
    给定一个非空正整数的数组,把数值与出现的次数存在一个新的字典中,并按照出现的次数排序
    定义一个函数,实现整数列表中偶数在左边,奇数在右边(不借助其它列表)
    python-leetcode1在一个整数数组中找出两数之和等于目标值,并把两数下标存在list中返回
    python根据key对字典进行排序
    python实现对浮点型的四舍五入
  • 原文地址:https://www.cnblogs.com/preminem/p/7754811.html
Copyright © 2011-2022 走看看