【问题标题】:Creating a namedtuple object using only a subset of arguments passed仅使用传递的参数子集创建 namedtuple 对象
【发布时间】:2013-07-12 19:14:39
【问题描述】:

我正在从 MySQL 数据库中提取行作为字典(使用 SSDictCursor)并使用以下方法进行一些处理:

from collections import namedtuple

class Foo(namedtuple('Foo', ['id', 'name', 'age'])):
    __slots__ = ()

    def __init__(self, *args):
        super(Foo, self).__init__(self, *args)

    # ...some class methods below here

class Bar(namedtuple('Bar', ['id', 'address', 'city', 'state']):
    __slots__ = ()

    def __init__(self, *args):
        super(Bar, self).__init__(self, *args)

    # some class methods here...

# more classes for distinct processing tasks...

要使用namedtuple,我必须事先确切知道我想要的字段,这很好。但是,我想允许用户将一个简单的SELECT * 语句输入到我的程序中,然后它将遍历结果集的行,使用这些不同的类执行多个任务。为了完成这项工作,我的类必须以某种方式检查从光标传入的 N 个字段,并仅获取与 namedtuple 定义所期望的名称相对应的特定子集 M

我的第一个想法是尝试编写一个可以应用于每个类的装饰器,它会检查该类以查看它所期望的字段,并仅将适当的参数传递给新对象。但是这几天我才开始阅读有关装饰器的内容,我对它们还没有那么自信。

所以我的问题分为两部分:

  1. 这是否可以使用单个装饰器来确定被装饰的特定类需要哪些字段?
  2. 是否存在具有相同功能且更易于使用、修改和理解的替代方案?

我有太多潜在的表和字段排列,每个结果集中有数百万行,只需要编写一个通用的namedtuple 子类来处理每个不同的任务。查询时间和可用内存已被证明是限制因素。

如果需要:

>>> sys.version
'2.7.5 (default, May 15 2013, 22:43:36) [MSC v.1500 32 bit (Intel)]'

【问题讨论】:

    标签: python arguments decorator namedtuple


    【解决方案1】:

    这些答案似乎都过于复杂。您真的想要新的类和重载,而不是仅仅编写一行代码或一个辅助函数来以您想要的方式实例化标准数据类型吗?

    Foo = namedtuple('Foo', ['id', 'name', 'age'], defaults=(None,) * 3)
    Bar = namedtuple('Bar', ['id', 'address', 'city', 'state'], defaults=(None,) * 4)
    
    poo = {'id': 1, 'age': 'Orz', 'city': 'Tucson', 'weight': True}
    ooh = {'id': 2, 'name': 'Spathi', 'state': 'Iowa', 'children': '25'}
    
    >>> Foo(*[poo[f] if f in poo else None for f in Foo._fields])
    Foo(id=1, name=None, age='Orz')
    

    哒哒!

    或者做个小帮手。

    # nt should have defaults
    def nt_from_kwargs(nt, **kwargs):
        return nt(**dict(i for i in kwargs.items() if i[0] in nt._fields))
    
    >>> nt_from_kwargs(Foo, id=1, age='Orz', city='Tucson', weight=True)
    Foo(id=1, name=None, age='Orz')
    
    >>> nt_from_kwargs(Bar, **poo)
    Bar(id=1, address=None, city='Tucson', state=None)
    
    >>> nt_from_kwargs(Bar, **{**poo, **ooh})
    Bar(id=2, address=None, city='Tucson', state='Iowa')
    

    每个人都喜欢字典。

    def nt_from_dict(nt, d):
        return nt(*[d[k] if k in d else None for k in nt._fields])
    
    >>> nt_from_dict(Foo, poo)
    Foo(id=1, name=None, age='Orz')
    
    >>> nt_from_dict(Bar, poo)
    Bar(id=1, address=None, city='Tucson', state=None)
    
    >>> nt_from_dict(Bar, {**poo, **ooh})
    Bar(id=2, address=None, city='Tucson', state='Iowa')
    

    【讨论】:

      【解决方案2】:

      首先,您必须覆盖 __new__ 以自定义 namedtuple 创建,因为 namedtuple__new__ 方法会在您到达 __init__ 之前检查其参数。

      其次,如果您的目标是接受和过滤关键字参数,您需要采用 **kwargs 并过滤并传递它,而不仅仅是 *args

      所以,把它放在一起:

      class Foo(namedtuple('Foo', ['id', 'name', 'age'])):
          __slots__ = ()
      
          def __new__(cls, *args, **kwargs):
              kwargs = {k: v for k, v in kwargs.items() if k in cls._fields}
              return super(Foo, cls).__new__(cls, *args, **kwargs)
      

      你可以用itemgetter替换那个dict理解,但是每次我使用带有多个键的itemgetter时,没有人理解它的含义,所以我很不情愿地停止使用它。


      如果您有理由这样做,您也可以覆盖__init__,因为它会在__new__ 返回Foo 实例时立即被调用。

      但是你不需要仅仅为了这个,因为 namedtuple 的 __init__ 不接受任何参数或做任何事情;这些值已经在__new__ 中设置(就像tuple 和其他不可变类型一样)。看起来对于 CPython 2.7,您实际上可以 super(Foo, self).__init__(*args, **kwargs) 并且它会被忽略,但是对于 PyPy 1.9 和 CPython 3.3,您会得到一个 TypeError。无论如何,没有理由通过它们,也没有说它应该起作用,所以即使在 CPython 2.7 中也不要这样做。

      请注意,您 __init__ 将获得未经过滤的 kwargs。如果你想改变它,你可以在__new__ 中就地改变kwargs,而不是制作一个新字典。但我相信这仍然不能保证做任何事情。它只是让它实现定义你是否得到过滤的参数或未过滤的,而不是保证未过滤的。


      那么,你能总结一下吗?当然!

      def LenientNamedTuple(name, fields):
          class Wrapper(namedtuple(name, fields)):
              __slots__ = ()
              def __new__(cls, *args, **kwargs):
                  args = args[:len(fields)]
                  kwargs = {k: v for k, v in kwargs.items() if k in fields}
                  return super(Wrapper, cls).__new__(cls, *args, **kwargs)
          return Wrapper
      

      请注意,这样做的好处是不必使用准私有/半记录的_fields 类属性,因为我们已经将fields 作为参数。

      此外,正如评论中所建议的那样,我添加了一行来丢弃任何多余的位置参数。


      现在您只需像使用 namedtuple 一样使用它,它会自动忽略任何多余的参数:

      class Foo(LenientNamedTuple('Foo', ['id', 'name', 'age'])):
          pass
      
      print(Foo(id=1, name=2, age=3, spam=4))
      

          print(Foo(1, 2, 3, 4, 5)) print(Foo(1, age=3, name=2, eggs=4))


      我已经上传了a test,在genexpr 上用dict() 替换了dict 理解,以实现2.6 的兼容性(2.6 是namedtuple 的最早版本),但没有截断args。它适用于 CPython 2.6.7、2.7.2、2.7.5、3.2.3、3.3.0 和 3.3.1、PyPy 1.9.0 中的位置、关键字和混合参数,包括无序关键字和 2.0b1,以及 Jython 2.7b。

      【讨论】:

      • 谢谢,我很欣赏细节。但是,当Wrapper 类中硬编码时,我看不到如何用一组字段声明Foo 和用另一组字段声明Bar
      • 经过一些实验,它似乎可以与class Wrapper(namedtuple(name, fields)) 一起使用。不能再可靠地采用 args-only 或混合 args/kwargs,但这对我来说不是问题。如果该行只是一个错误,请编辑,我会接受。
      • @AirThomas:哪里不能只使用 args 或混合 args?我只是在我拥有的每一个 Python 上测试了它,在我能想到的每一个 args 组合中,它总是按预期工作。请参阅已编辑答案中的链接。
      • len(args) != len(fields) 时不需要 args-only。我想我弄错了混合,这似乎工作正常。我猜正在发生的事情是,声明任何关键字都会提示非关键字参数在 kwargs 中隐式解析。
      • @AirThomas:实验一个简单的函数:def foo(*args, **kwargs): print(args, kwargs);这比通过阅读文档来找出规则的细节要容易得多。 (就个人而言,我经常忘记 2.x-vs.-3.x 的差异之一,并启动解释器来找出答案。)但无论如何,关于太多位置参数的好主意 - 尽管你可以做一点更简单;让我编辑答案。你不能真的允许太少的参数,除非你想提供一些默认值。
      【解决方案3】:

      namedtuple 类型有一个属性_fields,它是对象中字段名称的元组。您可以使用它从数据库记录中挖掘出所需的字段。

      【讨论】:

        猜你喜欢
        • 2015-08-17
        • 1970-01-01
        • 2023-04-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-10
        • 1970-01-01
        • 2020-02-05
        相关资源
        最近更新 更多