Как элегантно проверить, имеет ли словарь определенную структуру?
У меня есть словарь со следующей структурой:
D = {
'rows': 11,
'cols': 13,
(i, j): {
'meta': 'random string',
'walls': {
'E': True,
'O': False,
'N': True,
'S': True
}
}
}
# i ranging in {0 .. D['rows']-1}
# j ranging in {0 .. D['cols']-1}
Мне предлагается написать функцию, которая принимает произвольный объект в качестве аргумента и проверяет, имеет ли он эту структуру. Вот что я написал:
def well_formed(L):
if type(L) != dict:
return False
if 'rows' not in L:
return False
if 'cols' not in L:
return False
nr, nc = L['rows'], L['cols']
# I should also check the int-ness of nr and nc ...
if len(L) != nr*nc + 2:
return False
for i in range(nr):
for j in range(nc):
if not ((i, j) in L
and 'meta' in L[i, j]
and 'walls' in L[i, j]
and type(L[i, j]['meta']) == str
and type(L[i, j]['walls']) == dict
and 'E' in L[i, j]['walls']
and 'N' in L[i, j]['walls']
and 'O' in L[i, j]['walls']
and 'S' in L[i, j]['walls']
and type(L[i, j]['walls']['E']) == bool
and type(L[i, j]['walls']['N']) == bool
and type(L[i, j]['walls']['O']) == bool
and type(L[i, j]['walls']['S']) == bool):
return False
return True
Хотя это работает, мне это совсем не нравится. Есть ли способ сделать это на Pythonic?
Мне разрешено использовать только стандартную библиотеку.
Ответы
Ответ 1
Во-первых, я думаю, что больше "Pythonic" может потребовать прощения, а не разрешения - проверить, когда вам нужно было свойство, имело ли это свойство свойство.
Но, с другой стороны, это не поможет, если вас попросили создать что-то, чтобы проверить его на правильность.:)
Итак, если вам нужно проверить, вы можете использовать что-то вроде библиотеки схем, чтобы определить, как должна выглядеть ваша структура данных, а затем проверить другие структуры данных в отношении этой схемы.
Ответ 2
В Python точная идентификация задействованных типов менее важна, чем то, как ведут себя значения. Для определенного использования такого объекта достаточно ли этого объекта? Это означает, что L
не должен быть dict
, он просто имеет поддержку __getitem__
; L[(i,j)]['meta']
не должен быть str
, он просто должен поддерживать преобразование в строку через str(L[(i,j)]['meta'])
; и др.
Учитывая, что релаксация, я просто попытаюсь поймать любые ошибки, возникшие при попытке таких действий, и возвратить False
, если они произойдут. Например,
def well_formed(L):
try:
nr = L['rows']
nc = L['cols']
except KeyError:
return False
try:
for i in range(nr):
for j in range(nc):
str(L[(i,j)]['meta'])
walls = L[(i,j)]['walls']
for direction in walls:
# Necessary?
if direction not in "ENOS":
return False
if walls[direction] not in (True, False):
return False
except KeyError:
return False
return True
Учитывая, что любой объект имеет логическое значение, казалось бы бессмысленным попробовать bool(walls[direction])
; скорее, если иметь ровно True
или False
, поскольку значение не является жестким требованием, я бы просто проверил значение в целом. Аналогичным образом, дополнительные стены могут быть или не быть проблемой, и их не нужно явно проверять.
Ответ 3
Вы можете составить такую проверку (идея из экстракторов Scala). Преимущество состоит в том, что структура валидатора аналогична структуре для тестирования.
Недостатком является то, что многие вызовы функций могут сделать его намного медленнее.
class Mapping:
def __init__(self, **kwargs):
self.key_values = [KeyValue(k, v) for k, v in kwargs.items()]
def validate(self, to_validate):
if not isinstance(to_validate, dict):
return False
for validator in self.key_values:
if not validator.validate(to_validate):
return False
return True
class KeyValue:
def __init__(self, key, value):
self.key = key
self.value = value
def validate(self, to_validate):
return self.key in to_validate and self.value.validate(to_validate[self.key])
class Boolean:
def validate(self, to_validate):
return isinstance(to_validate, bool)
class Integer:
def validate(self, to_validate):
return isinstance(to_validate, int)
class String:
def validate(self, to_validate):
return isinstance(to_validate, str)
class CustomValidator:
def validate(self, to_validate):
if not Mapping(rows=Integer(), cols=Integer()).validate(to_validate):
return False
element_validator = Mapping(meta=String(), walls=Mapping(**{k: Boolean() for k in "EONS"}))
for i in range(to_validate['rows']):
for j in range(to_validate['cols']):
if not KeyValue((i, j), element_validator).validate(to_validate):
return False
return True
d = {
'rows': 11,
'cols': 13,
}
d.update({(i, j): {
'meta': 'random string',
'walls': {
'E': True,
'O': False,
'N': True,
'S': True
}
} for i in range(11) for j in range(13)})
assert CustomValidator().validate(d)
То же самое с переопределением isinstance (тестируется с Python 3.5)
class IsInstanceCustomMeta(type):
def __instancecheck__(self, instance):
return self.validate(instance)
def create_custom_isinstance_class(f):
class IsInstanceCustomClass(metaclass=IsInstanceCustomMeta):
validate = f
return IsInstanceCustomClass
def Mapping(**kwargs):
key_values = [KeyValue(k, v) for k, v in kwargs.items()]
def validate(to_validate):
if not isinstance(to_validate, dict):
return False
for validator in key_values:
if not isinstance(to_validate, validator):
return False
return True
return create_custom_isinstance_class(validate)
def KeyValue(key, value):
return create_custom_isinstance_class(lambda to_validate: key in to_validate and isinstance(to_validate[key], value))
def my_format_validation(to_validate):
if not isinstance(to_validate, Mapping(rows=int, cols=int)):
return False
element_validator = Mapping(meta=str, walls=Mapping(**{k: bool for k in "EONS"}))
for i in range(to_validate['rows']):
for j in range(to_validate['cols']):
if not isinstance(to_validate, KeyValue((i, j), element_validator)):
return False
return True
MyFormat = create_custom_isinstance_class(my_format_validation)
d = {
'rows': 11,
'cols': 13,
}
d.update({(i, j): {
'meta': 'random string',
'walls': {
'E': True,
'O': False,
'N': True,
'S': True
}
} for i in range(11) for j in range(13)})
assert isinstance(d, MyFormat)
Ответ 4
Если ваш формат был более простым, я бы согласился с другим ответом/комментариями использовать существующие библиотеки проверки схемы, такие как schema и voluptuous. Но, учитывая ваш конкретный случай проверки словаря с помощью ключей кортежа и значений этих кортежей в зависимости от значений других членов вашего dict, я думаю, вам лучше писать свой собственный валидатор, чем пытаться уговорить схему чтобы соответствовать вашему формату.
Ответ 5
from itertools import product
def isvalid(d):
try:
for key in product(range(d['rows']), range(d['cols'])):
sub = d[key]
assert (isinstance(sub['meta'], str) and
all(isinstance(sub['walls'][c], bool)
for c in 'EONS'))
except (KeyError, TypeError, AssertionError):
return False
return True
Если совместимость Python 2 важна или необходимо утверждать, что дополнительных ключей нет, сообщите мне.