键值对与字典概念讨论过 Julia 字典类型的基础知识。
由于Dict是一种重要且常用的集合类型,因此也有许多用来操作它们的函数。
获取条目最简单的方式是通过键(许多语言都是如此)。不过,如果请求一个不存在的键,就会得到KeyError。
julia> d = Dict(i => ('a':'c')[i] for i in 1:3)
Dict{Int64, Char} with 3 entries:
2 => 'b'
3 => 'c'
1 => 'a'
julia> d[2]
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
# missing key
julia> d[4]
ERROR: KeyError: key 4 not found
我们可以在尝试获取之前先检查一下键是否存在(比如用haskey(d, 4)),但直接为缺失的键取一个默认值往往也很有用。
为此,我们有get()函数,以及会修改原字典的变体get!()。二者的区别在于,是否会把“缺失”的键添加到Dict中。
julia> get(d, 4, '!')
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)
# get() leaves Dict unchanged
julia> d
Dict{Int64, Char} with 3 entries:
2 => 'b'
3 => 'c'
1 => 'a'
julia> get!(d, 4, '!')
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)
# get!() adds new key to Dict
julia> d
Dict{Int64, Char} with 4 entries:
4 => '!'
2 => 'b'
3 => 'c'
1 => 'a'
在前面的概念中我们看到,delete!()可以按键删除一个条目。借助pop!()函数,我们还可以把删除和获取值这两件事合并起来。
julia> d
Dict{Int64, Char} with 4 entries:
4 => '!'
2 => 'b'
3 => 'c'
1 => 'a'
# popped value is returned
julia> pop!(d, 3)
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
# Dict is shrunk
julia> d
Dict{Int64, Char} with 3 entries:
4 => '!'
2 => 'b'
1 => 'a'
迭代Dict时,默认会产出key => value键值对。可以用惯用的.first和.second来访问这两个组成部分。
# d is from the previous example
julia> [(kv.first, kv.second) for kv in d]
3-element Vector{Tuple{Int64, Char}}:
(4, '!')
(2, 'b')
(1, 'a')
很多时候,我们只想用键,或者只用值,这很容易做到。
julia> [k for k in keys(d)]
3-element Vector{Int64}:
4
2
1
julia> [v for v in values(d)]
3-element Vector{Char}:
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
合并两个Dict很有用,但我们需要说清楚:对于在多个输入中都出现的键,我们打算怎么处理。
merge()和merge!()函数会按从左到右的顺序合并任意数量的Dict。
已存在的键,其值会被直接覆盖,所以最右边的Dict会“胜出”。
julia> d3 = Dict("color" => "red", "shape" => "circle")
Dict{String, String} with 2 entries:
"shape" => "circle"
"color" => "red"
julia> d4 = Dict("color" => "blue", "coords" => (3, 2))
Dict{String, Any} with 2 entries:
"color" => "blue"
"coords" => (3, 2)
# color from d3 is lost
julia> merge(d3, d4)
Dict{String, Any} with 3 entries:
"shape" => "circle"
"color" => "blue"
"coords" => (3, 2)
mergewith()和mergewith!()函数允许我们把一个函数作为第一个参数传进去,用来定义重复的键应该如何合并。加法就是一个简单的例子。
julia> d5 = Dict("color" => "red", "area" => 3)
Dict{String, Any} with 2 entries:
"area" => 3
"color" => "red"
julia> d6 = Dict("shape" => "circle", "area" => 5)
Dict{String, Any} with 2 entries:
"shape" => "circle"
"area" => 5
# area values are added, not overwritten
julia> mergewith(+, d5, d6)
Dict{String, Any} with 3 entries:
"shape" => "circle"
"area" => 8
"color" => "red"
注意,mergewith系列函数是高阶函数在实际应用中的一个很好的例子。
Mecha Munch™ 是一家食品杂货购物自动化公司,最近刚聘请你参与他们的下单应用。你的团队负责打造一个 MVP(minimum viable product,最小可行产品),用来管理购物车的基本操作,让用户能够添加、移除和整理自己的杂货订单。好在,所有涉及金钱和结账的功能都由另一个团队负责!
这个 MVP 应该让用户能够把商品添加进购物车。既可以是单个商品,也可以一次添加多个商品。由于这只是一个 MVP,商品数量通过_重复出现_来表示。如果用户想添加 2 个 Oranges,输入的可迭代对象中就会出现两次 'Oranges'。如果购物车里已经有该商品,它的数量就加 1。如果该商品对购物车来说是_新_的,则应以数量 1 添加。
创建函数add_items!(<cart>, <items>),它接受一个购物车字典和一个待添加商品的可迭代对象作为参数。函数应返回更新后的用户购物车字典。
julia> additems!(Dict("Banana" => 3, "Apple" => 2, "Orange" => 1),
("Apple", "Apple", "Orange", "Apple", "Banana"))
Dict{String, Int64} with 3 entries:
"Apple" => 5
"Orange" => 2
"Banana" => 4
julia> additems!(Dict("Banana" => 3, "Apple" => 2, "Orange" => 1),
["Banana", "Orange", "Blueberries", "Banana"])
Dict{String, Int64} with 4 entries:
"Blueberries" => 1
"Apple" => 2
"Orange" => 2
"Banana" => 5
应用里有一个“灵感”板块,里面是来自各种菜系的成品食谱。用户可以选择其中任意一份食谱,把它的所有食材自动添加到购物车。项目经理让你想个办法来编辑这些“灵感”食谱,因为内容团队总在调整食材和用量。
创建函数update_recipes!(<ideas>, <updates>),它接受一个“灵感”字典和一个食谱更新字典作为参数。函数应返回更新后的“灵感”字典。
julia>update_recipes!(
Dict("Banana Bread" => Dict("Banana" => 1, "Apple" => 1, "Walnuts" => 1, "Flour" => 1, "Eggs" => 2, "Butter" => 1),
"Raspberry Pie" => Dict("Raspberry" => 1, "Orange" => 1, "Pie Crust" => 1, "Cream Custard" => 1)),
Dict("Banana Bread" => Dict("Banana" => 4, "Walnuts" => 2, "Flour" => 1, "Butter" => 1, "Milk" => 2, "Eggs" => 3))
)
Dict{String, Dict{String, Int64}} with 2 entries:
"Raspberry Pie" => Dict("Cream Custard"=>1, "Raspberry"=>1, "Orange"=>1, "Pie Crust"=>1)
"Banana Bread" => Dict("Eggs"=>3, "Flour"=>1, "Milk"=>2, "Butter"=>1, "Walnuts"=>2, "Banana"=>4)
julia> update_recipes!(
Dict("Banana Bread" => Dict("Banana" => 1, "Apple" => 1, "Walnuts" => 1, "Flour" => 1, "Eggs" => 2, "Butter" => 1),
"Raspberry Pie" => Dict("Raspberry" => 1, "Orange" => 1, "Pie Crust" => 1, "Cream Custard" => 1),
"Pasta Primavera" => Dict("Eggs" => 1, "Carrots" => 1, "Spinach" => 2, "Tomatoes" => 3, "Parmesan" => 2, "Milk" => 1, "Onion" => 1)),
Dict("Raspberry Pie" => Dict("Raspberry" => 3, "Orange" => 1, "Pie Crust" => 1, "Cream Custard" => 1, "Whipped Cream" => 2),
"Pasta Primavera" => Dict("Eggs" => 1, "Mixed Veggies" => 2, "Parmesan" => 2, "Milk" => 1, "Spinach" => 1, "Bread Crumbs" => 1),
"Blueberry Crumble" => Dict("Blueberries" => 2, "Whipped Creme" => 2, "Granola Topping" => 2, "Yogurt" => 3))
)
Dict{String, Dict{String, Int64}} with 4 entries:
"Raspberry Pie" => Dict("Raspberry"=>3, "Orange"=>1, "Pie Crust"=>1, "Cream Custard"=>1, "Whipped Cream"=>2)
"Pasta Primavera" => Dict("Eggs"=>1, "Mixed Veggies"=>2, "Parmesan"=>2, "Milk"=>1, "Spinach"=>1, "Bread Crumbs"=>1)
"Blueberry Crumble" => Dict("Blueberries"=>2, "Whipped Creme"=>2, "Granola Topping"=>2, "Yogurt"=>3)
"Banana Bread" => Dict("Banana"=>1, "Apple"=>1, "Walnuts"=>1, "Flour"=>1, "Eggs"=>2, "Butter"=>1)
应用需要把指定用户的购物车发送到门店备货。不过,门店里的店员需要知道商品放在哪里,这要靠一个唯一的货架通道编号。为了让店员更高效,“备货清单”需要按商品的货架通道编号排序。
创建函数send_to_store(<cart>, <aislecodes>),它接受一个用户购物车和一个记录了门店货架编号的字典。函数应返回一份合并后的“备货清单”,其中顾客订购的每件商品都对应一个aislecode => quantity键值对。各商品应按各自的货架通道编号顺序排列。
julia> cart = Dict("Orange" => 1, "Milk" => 2, "Banana" => 3, "Apple" => 2, "Chocolate" => 2);
julia> aislecodes = Dict("Orange" => 2.4, "Milk" => 3.3, "Banana" => 2.6, "Apple" => 2.2, "Chocolate" => 5.7);
julia> send_to_store(cart, aislecodes)
5-element Vector{Pair{Float64, Int64}}:
2.2 => 2
2.4 => 1
2.6 => 3
3.3 => 2
5.7 => 2
应用不能无休止地接受顾客下单。总有一天,门店的各种商品会卖光。所以你的 MVP 需要在用户每次把订单发送到门店时更新门店库存。否则,顾客就会订购到实际上并不存在的商品。
创建函数update_store_inventory!(<inventory>, <cart>),它接受一个cart和一个inventory。函数应按“购物车”中“已订购”的数量扣减门店库存,然后返回一个字典,包含所有缺货的商品(也就是数量已经降到0的商品)。
julia> inventory = Dict("Banana" => 15, "Apple" => 12, "Orange" => 1, "Milk" => 4);
julia> cart = Dict("Orange" => 1, "Milk" => 2, "Banana" => 3, "Apple" => 2);
julia> update_store_inventory!(inventory, cart)
Dict{String, Int64} with 1 entry:
"Orange" => 0
julia> inventory
Dict{String, Int64} with 4 entries:
"Milk" => 2
"Apple" => 10
"Orange" => 0
"Banana" => 12
除了更新库存,你的应用还能帮忙补货。顾客下单后,对于任何缺货的商品,你的 MVP 都应该能准备一份小订单。这会简化触发大订单的流程。此外,有时会有不在库存清单里的限时促销商品。如果这些商品在促销期结束前就卖光了,说明它们很受欢迎,可以加入库存清单。
创建reorder!(<outofstock>, <stock>),它接受outofstock和stock。stock字典包含标准库存商品及其补货数量。outofstock字典是update_store_inventory!的结果,也就是数量为零的商品字典。应把outofstock中各商品的数量更新为stock中的数量并返回。此外,outofstock里凡是不在stock中的商品,都应以默认数量100加入stock。
julia> inventory = Dict("Banana" => 15, "Apple" => 12, "Orange" => 1, "Milk" => 4, "Chocolate" => 2);
julia> cart = Dict("Orange" => 1, "Milk" => 2, "Banana" => 3, "Apple" => 2, "Chocolate" => 2);
julia> stock = Dict("Banana" => 150, "Apple" => 120, "Orange" => 120, "Milk" => 45);
julia> outofstock = update_store_inventory!(inventory, cart)
Dict{String, Int64} with 1 entry:
"Orange" => 0
"Chocolate" => 0
julia> reorder!(outofstock, stock)
Dict{String, Int64} with 1 entry:
"Orange" => 120
"Chocolate" => 100
julia> stock
Dict{String, Int64} with 5 entries:
"Milk" => 45
"Apple" => 120
"Chocolate" => 100
"Orange" => 120
"Banana" => 150