【问题标题】:Make namedtuple accept kwargs让 namedtuple 接受 kwargs
【发布时间】:2016-06-14 03:15:27
【问题描述】:

如果我有这样的课程:

class Person(object):
    def __init__(self, name, **kwargs):
        self.name = name

p = Person(name='joe', age=25) # age is ignored

多余的参数被忽略。但如果我有一个namedtuple,我会得到`unexpected keyword argument:

from collections import namedtuple 

Person = namedtuple('Person', 'name')
p = Person(name='joe', age=25)

# Traceback (most recent call last):
#   File "python", line 1, in <module>
# TypeError: __new__() got an unexpected keyword argument 'age'

我怎样才能让namedtuple 接受kwargs 以便我可以安全地传递额外的参数?

【问题讨论】:

  • 名字中的线索。

标签: python python-2.7 keyword-argument namedtuple


【解决方案1】:

@paulmcg 使用工厂方法的答案略有不同:

_Person = namedtuple('Person', 'name')

class Person(_Person):
    @staticmethod
    def from_dict(args):
        args = {k: v for k, v in args.items() if k in _Person._fields}
        return Person(**args)


p = Person.from_dict(dict(name='joe', age=25))
print(p)

【讨论】:

    【解决方案2】:

    解释器中的以下会话显示了解决问题的一种可能解决方案:

    Python 3.5.0 (v3.5.0:374f501f4567, Sep 13 2015, 02:27:37) [MSC v.1900 64 bit (AMD64)] on win32
    Type "copyright", "credits" or "license()" for more information.
    >>> import collections
    >>> class Person(collections.namedtuple('base', 'name')):
        __slots__ = ()
        def __new__(cls, *args, **kwargs):
            for key in tuple(kwargs):
                if key not in cls._fields:
                    del kwargs[key]
            return super().__new__(cls, *args, **kwargs)
    
    
    >>> p = Person(name='joe', age=25)
    >>> p
    Person(name='joe')
    >>> 
    

    替代方案:

    由于您希望有一个更简单的解决方案,您可能会发现下一个程序更符合您的喜好:

    #! /usr/bin/env python3
    import collections
    
    
    def main():
        Person = namedtuple('Person', 'name')
        p = Person(name='joe', age=25)
        print(p)
    
    
    def namedtuple(typename, field_names, verbose=False, rename=False):
        base = collections.namedtuple('Base', field_names, verbose, rename)
        return type(typename, (base,), {
            '__slots__': (),
            '__new__': lambda cls, *args, **kwargs: base.__new__(cls, *args, **{
                key: value for key, value in kwargs.items()
                if key in base._fields})})
    
    
    if __name__ == '__main__':
        main()
    

    【讨论】:

    • 好吧,如果我想为我需要的每个类编写一个__new__ 方法,那么我将像我的第一个示例一样创建一个类。不过谢谢你的回答。
    • @norbertpy 我添加了一个新的namedtuple 实现来自动为您解决这个问题。它与第一个示例执行相同的操作,但处理对__new__ 的更改,因此您可以避免自己编写代码。
    • 不要创建没有__slots__ 的namedtuple 的子类。它浪费内存。
    【解决方案3】:

    可以包装 Person 类构造函数以忽略未定义为 Person 命名元组字段的参数:

    from collections import namedtuple
    Person = namedtuple('Person', 'name')
    
    def make_person(*args, **kwargs):
        person_args = {}
    
        # process positional args
        if len(args) > len(Person._fields):
            msg = "Person() takes %d positional arguments but %d were given" % (len(Person._fields), len(args))
            raise TypeError(msg)
        for arg_name, arg_value in zip(Person._fields, args):
            person_args[arg_name] = arg_value
    
        # process keyword args
        for arg_name, arg_value in kwargs.items():
            try:
                i = Person._fields.index(arg_name)
            except ValueError:
                pass # ignore arguments not defined as Person fields
            else:
                if arg_name in person_args:
                    msg = "make_person() got multiple values for argument " + repr(arg_name)
                    raise TypeError(msg)
                person_args[arg_name] = arg_value
    
        if len(person_args) != len(Person._fields):
            msg = "Person() requires additional arguments: "
            msg += ", ".join([repr(x) for x in Person._fields if x not in person_args])
            raise TypeError(msg)
        return Person(*[person_args[x] for x in Person._fields])
    

    鉴于上述情况:

    >>> make_person('a')
    Person(name='a')
    >>> make_person('a', b='b')
    Person(name='a')
    >>> make_person('a', name='b')
    TypeError: make_person() got multiple values for argument 'name'
    >>> make_person(b='b')
    TypeError: Person() requires additional arguments: 'name'
    >>> make_person(1, 2)
    TypeError: Person() takes 1 positional arguments but 2 were given
    

    【讨论】:

    • 与 Paul 的答案不同,此答案支持正确组合位置参数和关键字参数,并正确处理所有带有信息错误消息的边缘情况。
    【解决方案4】:

    不漂亮:

    p = Person(*(dict(name='joe', age=25)[k] for k in Person._fields))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-05
      • 2010-12-06
      • 2018-04-28
      • 2018-11-18
      • 2021-06-16
      • 2014-04-28
      相关资源
      最近更新 更多