仅使用传递的一部分参数创建命名元组对象

3
我正在使用SSDictCursor从MySQL数据库中提取行作为字典,并进行一些处理,采用以下方法:
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 < N。
我的第一个想法是尝试编写一个可以应用于每个类的单个装饰器,该装饰器将检查类以查看它需要哪些字段,并仅将适当的参数传递给新对象。但是,我最近几天才开始阅读有关装饰器的内容,对它们还不太自信。
因此,我的问题分为两部分:
1.是否可能使用单个装饰器完成此操作,以找出由特定类进行装饰所需的字段?
2.是否有一种具有相同功能的替代方法,可以更易于使用、修改和理解?
我有太多的表格和字段组合,每个结果集中都有数百万行,无法编写一个通用的namedtuple子类来处理每个不同的任务。查询时间和可用内存已经成为限制因素。
如果需要:
>>> sys.version
'2.7.5 (default, May 15 2013, 22:43:36) [MSC v.1500 32 bit (Intel)]'
3个回答

4
首先,您需要重写__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替换那个字典推导式,但是每次我带有多个键的使用itemgetter时,没有人理解它的意思,所以我不情愿地停止了使用。


如果你有理由这样做,你也可以重写__init__,因为它会在__new__返回一个Foo实例后立即调用。

但是就为了这个,你不需要这样做,因为命名元组的__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))

我已经上传了一个测试文件,其中将字典推导式替换为dict()以适应2.6版本(2.6是最早的带有namedtuple的版本),但没有截断参数。 它可以在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 - undefined
@AirThomas:它在哪些情况下无法接受仅参数或混合参数?我刚刚在我拥有的每个Python上进行了测试,尝试了各种参数组合,它总是按预期工作。请参阅编辑后答案中的链接。 - undefined
LenientNamedTuple.__new__内,可以处理len(args) > len(fields)(例如通过截断)并保留len(args) < len(fields)不变。不过,我认为根据我的需求,完全禁止非关键字参数更好。按照编程哲学的原则:显式、简单。 - undefined
1
@AirThomas:尝试一个简单的函数:def foo(*args, **kwargs): print(args, kwargs);这比阅读文档来弄清规则的细枝末节要容易得多。(就我个人而言,我经常忘记2.x和3.x之间的差异之一,并启动一个解释器找出答案。)但无论如何,关于参数过多的想法很好-尽管你可以用更简单的方法来做;让我编辑一下答案。除非您想提供一些默认值,否则不能允许参数过少。 - undefined
我应该澄清一下,通过“leave it alone”,我是指让它继续抛出异常。 :) - undefined
显示剩余2条评论

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

3
这些答案都显得过于复杂。你是否真的想要新建类和重载,而不是只编写一行代码或帮助函数来实例化标准数据类型以满足你的需求?
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')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接