我有一个简单的NamedTuple,我想强制执行约束。有可能吗?
采用以下示例:
from typing import NamedTuple
class Person(NamedTuple):
first_name: str
last_name: str
如果我对名称字段有一个所需的最大长度(例如50个字符),我怎样才能确保你不能创建名字长于此的Person对象?
通常,如果这只是一个类,而不是一个NamedTuple,我会使用@property
,@attr.setter
处理此问题,并覆盖__init__
方法。但是,NamedTuples不能拥有__init__
,我无法看到只为其中一个属性设置的方法(如果可以的话,我不知道是否在构造,NamedTuple甚至会使用它。)
那么,这可能吗?
注意:我特别想使用一个NamedTuple(而不是试图通过我自己的方法/魔法使一个类不可变)
答案 0 :(得分:1)
所以我编写了一些基本上符合我想要的东西。我忘记在这里发帖了,所以它从我原来的问题中略有进化,但我认为我最好在这里发帖,以便其他人可以根据需要使用它。
import inspect
from collections import namedtuple
class TypedTuple:
_coerce_types = True
def __new__(cls, *args, **kwargs):
# Get the specified public attributes on the class definition
typed_attrs = cls._get_typed_attrs()
# For each positional argument, get the typed attribute, and check it's validity
new_args = []
for i, attr_value in enumerate(args):
typed_attr = typed_attrs[i]
new_value = cls.__parse_attribute(typed_attr, attr_value)
# Build a new args list to construct the namedtuple with
new_args.append(new_value)
# For each keyword argument, get the typed attribute, and check it's validity
new_kwargs = {}
for attr_name, attr_value in kwargs.items():
typed_attr = (attr_name, getattr(cls, attr_name))
new_value = cls.__parse_attribute(typed_attr, attr_value)
# Build a new kwargs object to construct the namedtuple with
new_kwargs[attr_name] = new_value
# Return a constructed named tuple using the named attribute, and the supplied arguments
return namedtuple(cls.__name__, [attr[0] for attr in typed_attrs])(*new_args, **new_kwargs)
@classmethod
def __parse_attribute(cls, typed_attr, attr_value):
# Try to find a function defined on the class to do checks on the supplied value
check_func = getattr(cls, f'_parse_{typed_attr[0]}', None)
if inspect.isroutine(check_func):
attr_value = check_func(attr_value)
else:
# If the supplied value is not the correct type, attempt to coerce it if _coerce_type is True
if not isinstance(attr_value, typed_attr[1]):
if cls._coerce_types:
# Coerce the value to the type, and assign back to the attr_value for further validation
attr_value = typed_attr[1](attr_value)
else:
raise TypeError(f'{typed_attr[0]} is not of type {typed_attr[1]}')
# Return the original value
return attr_value
@classmethod
def _get_typed_attrs(cls) -> tuple:
all_items = cls.__dict__.items()
public_items = filter(lambda attr: not attr[0].startswith('_') and not attr[0].endswith('_'), all_items)
public_attrs = filter(lambda attr: not inspect.isroutine(attr[1]), public_items)
return [attr for attr in public_attrs if isinstance(attr[1], type)]
这是我的TypedTuple类,它基本上表现得像NamedTuple,除了你得到类型检查。它具有以下基本用法:
>>> class Person(TypedTuple):
... """ Note, syntax is var=type, not annotation-style var: type
... """
... name=str
... age=int
...
>>> Person('Dave', 21)
Person(name='Dave', age=21)
>>>
>>> # Like NamedTuple, argument order matters
>>> Person(21, 'dave')
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'dave'
>>>
>>> # Can used named arguments
>>> Person(age=21, name='Dave')
Person(name='Dave', age=21)
所以现在你有了一个命名元组,它的行为方式基本相同,但它会键入检查你提供的参数。
默认情况下,TypedTuple还会尝试将您提供的数据强制转换为您应该提供的类型:
>>> dave = Person('Dave', '21')
>>> type(dave.age)
<class 'int'>
可以关闭此行为:
>>> class Person(TypedTuple):
... _coerce_types = False
... name=str
... age=int
...
>>> Person('Dave', '21')
Traceback (most recent call last):
...
TypeError: age is not of type <class 'int'>
最后,您还可以指定特殊的解析方法,可以执行任何特定的检查或强制执行。这些方法具有命名约定_parse_ATTR
:
>>> class Person(TypedTuple):
... name=str
... age=int
...
... def _parse_age(value):
... if value < 0:
... raise ValueError('Age cannot be less than 0')
...
>>> Person('dave', -3)
Traceback (most recent call last):
...
ValueError: Age cannot be less than 0
我希望其他人觉得这很有用。
(请注意,此代码仅适用于Python3)
答案 1 :(得分:0)
您将不得不重载构造子类的__new__
方法。
这是一个在__new__
内定义名称检查函数并检查每个参数的示例。
from collections import namedtuple
# create the named tuple
BasePerson = namedtuple('person', 'first_name last_name')
# subclass the named tuple, overload new
class Person(BasePerson):
def __new__(cls, *args, **kwargs):
def name_check(name):
assert len(name)<50, 'Length of input name "{}" is too long'.format(name)
# check the arguments
for a in args + tuple(kwargs.values()):
name_check(a)
self = super().__new__(cls, *args, **kwargs)
return self
现在我们可以测试一些输入......
Person('hello','world')
# returns:
Person(first_name='hello', last_name='world')
Person('hello','world'*10)
# raises:
AssertionError Traceback (most recent call last)
<ipython-input-42-1ee8a8154e81> in <module>()
----> 1 Person('hello','world'*10)
<ipython-input-40-d0fa9033c890> in __new__(cls, *args, **kwargs)
12 # check the arguments
13 for a in args + tuple(kwargs.values()):
---> 14 name_check(a)
15
16 self = super().__new__(cls, *args, **kwargs)
<ipython-input-40-d0fa9033c890> in name_check(name)
8 def __new__(cls, *args, **kwargs):
9 def name_check(name):
---> 10 assert len(name)<50, 'Length of input name "{}" is too long'.format(name)
11
12 # check the arguments
AssertionError: Length of input name "worldworldworldworldworldworldworldworldworldworld" is too long