你对Python3 NamedTuple属性有约束吗?

时间:2017-10-27 11:42:48

标签: python python-3.x

我有一个简单的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(而不是试图通过我自己的方法/魔法使一个类不可变)

2 个答案:

答案 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