今天在写一个检查输入数据类型的小程序,发现这个问题很有意思,可以考验下大家思考问题是否全面,对Python中的数据结构理解是否准确。大家可以思考一下,然后尝试写一下这个方法的实现。
问题的背景是我们从YAML格式的文件中读取了一个配置,我们需要检查配置是否符合格式,并且对于支持输入列表的地方,比如:
scripts:
- abc如果列表只有一项,也允许写成
scripts: abc并且某些字段可能接受多种不同的格式,比如既可以简单输入字符串,也可以输入包含更多信息的对象。需要在检查时检查能否匹配到任意格式之一。
要实现的方法接口是这样的:
def check_type(value, type):
...其中value是一个普通的Python的值,可能包括列表、字典以及它们的嵌套等等。要检查的type比较有意思,它可以像Java之类的泛型一样,指定列表当中的值的类型之类。在这里我们是这么规定的(就不按标准的方式写了,意思到就行):
type ::= python_type
| None
| (type, type, ...)
| ()
| []
| [type]
| {}
| {"key": type, "key": type, ...}也就是说type可以是:
- 任意合法的Python类型,比如str,int,list等。注意object可以匹配所有可能的值(包括None)
- None,只能匹配None。由于Python中None是个特殊的type的实例,也就相当于types.NoneType。
- 一个元组,其中每个元素是一个合法的type(注意:不一定是Python类型,而是递归定义的type)。表示可以匹配其中任意一个类型,返回第一个成功匹配的结果。比如说(str, None),可以匹配str类型,或者None。(str, int)可以匹配str或者int。(str, [str])可以匹配str或者[str]。
- 特殊的,如果是一个空的元组(),表示任意非None的值。
- 一个列表,其中仅包含一个元素,这个元素是个合法的type。它能匹配两种值:如果是一个list或者tuple,则要求其中所有的元素都能匹配内部的type,返回时自动将tuple也转换成list;如果是非list的值,则这个值必须匹配内部的type,在返回时会自动将这个值转换成包含一个元素的列表,比如"abc"在匹配[str]的时候,会成功匹配并返回["abc"]。注意[list]只能匹配[[1]],而不能匹配[1] —— 在有歧义的情况下不允许自动转换。同时注意[[str]]可以匹配"abc"——因为"abc"可以匹配str,进而可以匹配[str],因此可以匹配[[str]]。虽然有点奇怪不过这的确是允许的。
- 如果是空列表,等同于[object]——注意,它代表如果这个位置是个非list或tuple的值,就将它转换为包含一个值的列表;否则保留list。
- 如果是字典,表示这个值必须是个字典。
- 如果这个字典中包含了一些key,则相应的key在字典中对应值的类型会进一步做检查。type字典中每个key对应的值也必须是合法的type。key必须是个字符串。为了让这个功能更有用,我们规定:
- 如果key以?开头,比如"?abc",表示"abc"是一个可选的key,可以不存在,但如果存在则必须匹配相应的type。
- 如果key以!开头,比如"!abc",表示"abc"是一个必须存在的key,如果不存在则不能成功匹配。
- 如果key以~开头,比如"~abc.*def",表示任意匹配~之后的正则表达式的key都需要符合相应的type。例外的情形是,如果某个key已经匹配到了前两种情况,则不再考虑正则表达式的规则,这样可以用"~"简单匹配所有没有定义的其他的key。于是可以用"~": NoMatchType (NoMatchType是一个不可以构造的用户类,所以任意对象都不是它的实例)来防止出现定义以外的key。
- 其他情况,视同前面有!,也就是必选的key。
不知道是否说清楚了,我们来举几个例子:
>>> check_type("abc", str) "abc" >>> check_type([1,2,3], [int]) [1,2,3] >>> check_type((1,2,3), [int]) [1,2,3] >>> check_type([1,2,"abc"], [int]) (Exception...) >>> check_type("abc", [str]) ["abc"] >>> check_type(None, str) (Exception...) >>> check_type(None, (str, None)) None >>> check_type([1,2,"abc",["def","ghi"]], [(int, [str])]) [1,2,["abc"],["def","ghi"]] >>> check_type({"abc":123, "def":"ghi"}, {"abc": int, "def": str}) {"abc":123, "def":"ghi"} >>> check_type({"abc": {"def": "test", "ghi": 5}, "def": 1}, {"abc": {"def": str, "ghi": int}, "def": [int]}) {"abc": {"def": "test", "ghi": 5}, "def": [1]}聪明的读者,你想到应该怎样实现这个方法了吗?自己动脑思考一下,动手练习一下吧,看看你的实现能够处理哪些奇奇怪怪的情况,思考下在哪些输入时会不会有bug。
小提示
推荐先思考后再看
这个问题有一个巨大的陷阱。
在Python中,像list,dict这样的类型由于保存引用,是可以自己包含自己的,形成递归的结构,比如:
a = [] a.append(a)按我们的定义,它是一个完全合法的类型,它可以匹配任意多层、任意结构的只由括号组成的数据,比如[[],[[]],[[],[]]]。
更严重的是,输入数据也可能包含这样的递归。想象一下你的程序执行下面这条语句的时候会怎么样?
a = [] a.append(a) check_type(a, a)某些规则要求将输入数据进行修改然后返回,比如"abc"在匹配[str]的时候需要返回["abc"],这些规则也需要对递归数据生效,比如
my_type = [] my_type.append(([str], my_type)) my_data = ["abc"] my_data.append(my_data) check_type(my_data, my_type) # should return [["abc"], [["abc"], ...]]递归结构修改之后也必须要返回递归结构,这会是一个实现上的难点。
同一个包含递归定义的对象,可能会匹配到不同的类型,从而生成不同的值,不一定一个源对象就对应一个目的对象:
my_type = {} my_type["abc"] = my_type my_type["def"] = [my_type] my_data = {} my_data["abc"] = my_data my_data["def"] = my_data check_type(my_data, my_type) # should return {"abc": {"abc": {...}, "def": [{...}]}, "def": [{"abc": {...}, "def": [{...}]}]}你的程序是否能够正确处理这些情形呢?
题外话,许多人也许不知道,YAML当中真的是可以输入这样的递归的结构的,只要运用anchor就行了:
其实递归结构处理不好也不能怪大家,你看CPython自己>>> import yaml >>> data=""" ... &my_data ... abc: ... abc: 1 ... def: *my_data ... """ >>> a = yaml.safe_load(data) >>> a {'abc': {'abc': 1, 'def': {...}}} >>> a['abc']['def'] {'abc': {'abc': 1, 'def': {...}}}>>> a = [] >>> a.append(a) >>> b = [] >>> b.append(b) >>> a == b Traceback (most recent call last): File "<stdin>", line 1, in <module> RecursionError: maximum recursion depth exceeded in comparison