在“编译”时不知道属性名称的情况下,在 Python 中复制 Google App Engine 数据存储区中的实体

Posted

技术标签:

【中文标题】在“编译”时不知道属性名称的情况下,在 Python 中复制 Google App Engine 数据存储区中的实体【英文标题】:Copy an entity in Google App Engine datastore in Python without knowing property names at 'compile' time 【发布时间】:2011-02-10 21:03:20 【问题描述】:

在我正在编写的 Python Google App Engine 应用程序中,我有一个存储在数据存储区中的实体,我需要检索该实体,制作它的精确副本(密钥除外),然后将该实体放回去在。

我该怎么做?特别是,在执行此操作时是否需要注意任何警告或技巧,以便获得我期望的副本而不是其他内容。

ETA:好吧,我试过了,但确实遇到了问题。我想以这样一种方式制作我的副本,这样我在编写代码时就不必知道属性的名称。我的想法是这样做:

#theThing = a particular entity we pull from the datastore with model Thing
copyThing = Thing(user = user)
for thingProperty in theThing.properties():
    copyThing.__setattr__(thingProperty[0], thingProperty[1])

这执行没有任何错误...直到我尝试从数据存储中提取 copyThing,此时我发现所有属性都设置为 None(显然除了用户和密钥)。很明显,这段代码正在做某事,因为它用 None 替换了默认值(所有属性都设置了默认值),但根本不是我想要的。有什么建议吗?

【问题讨论】:

【参考方案1】:

我既不是 Python 也不是 AppEngine 专家,但不能动态获取/设置属性吗?

props = 
for p in Thing.properties():
    props[p] = getattr(old_thing, p)
new_thing = Thing(**props).put()

【讨论】:

(自我说明:在回答之前阅读整个问题,而不仅仅是主题。)【参考方案2】:

给你:

def clone_entity(e, **extra_args):
  """Clones an entity, adding or overriding constructor attributes.

  The cloned entity will have exactly the same property values as the original
  entity, except where overridden. By default it will have no parent entity or
  key name, unless supplied.

  Args:
    e: The entity to clone
    extra_args: Keyword arguments to override from the cloned entity and pass
      to the constructor.
  Returns:
    A cloned, possibly modified, copy of entity e.
  """
  klass = e.__class__
  props = dict((k, v.__get__(e, klass)) for k, v in klass.properties().iteritems())
  props.update(extra_args)
  return klass(**props)

示例用法:

b = clone_entity(a)
c = clone_entity(a, key_name='foo')
d = clone_entity(a, parent=a.key().parent())

编辑:使用 NDB 时的更改

将 Gus 下面的评论与对指定不同数据存储名称的属性的修复相结合,以下代码适用于 NDB:

def clone_entity(e, **extra_args):
  klass = e.__class__
  props = dict((v._code_name, v.__get__(e, klass)) for v in klass._properties.itervalues() if type(v) is not ndb.ComputedProperty)
  props.update(extra_args)
  return klass(**props)

示例用法(注意 key_name 在 NDB 中变为 id):

b = clone_entity(a, id='new_id_here')

旁注:请参阅_code_name 的使用以获取 Python 友好的属性名称。如果没有这个,像name = ndb.StringProperty('n') 这样的属性会导致模型构造函数引发AttributeError: type object 'foo' has no attribute 'n'

【讨论】:

这对我不起作用:AttributeError: type object 'NoneType' has no attribute 'properties' @Rosarch 听起来您正在将 None 传递给 clone_entity 方法。自然,您不能克隆“无”。 干净整洁,但如果需要,它会从数据存储中提取参考属性。例如如果实体有 20 个 ReferenceProperty 字段,那么它们将按顺序从数据存储中提取。 (20 次往返很糟糕 :) 这可以通过检查 db.ReferenceProperty 并使用 get_value_for_datastore 来解决。 确保手动设置任何 db.DateTimeProperty 为 auto_now_add=True,因为克隆会复制原始实体创建的日期时间。 (除非那是你想做的) 对于使用 NDB 的下一代,您必须将 klass.properties() 更改为 klass._properties。你会得到一个计算属性的错误,所以要小心。我为此替换了 props var:props = dict((k, v.__get__(e, klass)) for k, v in klass._properties.iteritems() if type(v) is not ndb.ComputedProperty)【参考方案3】:

这只是对Nick Johnson's excellent code 的扩展,以解决 Amir 在 cmets 中强调的问题:

    不再通过不必要的数据存储往返来检索 ReferenceProperty 的 db.Key 值。 您现在可以使用 auto_now 和/或 auto_now_add 标志指定是否要跳过 DateTime 属性。

这是更新后的代码:

def clone_entity(e, skip_auto_now=False, skip_auto_now_add=False, **extra_args):
  """Clones an entity, adding or overriding constructor attributes.

  The cloned entity will have exactly the same property values as the original
  entity, except where overridden. By default it will have no parent entity or
  key name, unless supplied.

  Args:
    e: The entity to clone
    skip_auto_now: If True then all DateTimeProperty propertes will be skipped which have the 'auto_now' flag set to True
    skip_auto_now_add: If True then all DateTimeProperty propertes will be skipped which have the 'auto_now_add' flag set to True
    extra_args: Keyword arguments to override from the cloned entity and pass
      to the constructor.
  Returns:
    A cloned, possibly modified, copy of entity e.
  """

  klass = e.__class__
  props = 
  for k, v in klass.properties().iteritems():
    if not (type(v) == db.DateTimeProperty and ((skip_auto_now and getattr(v, 'auto_now')) or (skip_auto_now_add and getattr(v, 'auto_now_add')))):
      if type(v) == db.ReferenceProperty:
        value = getattr(klass, k).get_value_for_datastore(e)
      else:
        value = v.__get__(e, klass)
      props[k] = value
  props.update(extra_args)
  return klass(**props)

第一个if 表达式不是很优雅,如果您能分享更好的编写方法,我不胜感激。

【讨论】:

感谢您撰写本文。这正是我一直在寻找的。它确实需要改变。即使这不是对象方法,也有对 self 的引用。 value = v.__get__(self, klass)这一行需要改为value = v.__get__(e, klass) 哎呀,我错过了对自我的第二次提及。 value = getattr(klass, k).get_value_for_datastore(self) 应该变成 value = getattr(klass, k).get_value_for_datastore(e) 已更正。谢谢和抱歉! 我想知道为什么不直接使用_properties.iteritems,因为结果键只是属性名称。【参考方案4】:

如果您使用的是 NDB,您可以简单地复制: new_entity.populate(**old_entity.to_dict())

【讨论】:

如何修改克隆的键名? to_dict() 返回 _properties 列表中的内容...其中可能包括模型类中不存在的属性(已删除)。这将导致错误“类型对象'[您的模型名称]'没有属性'已删除的属性'”【参考方案5】:

如果您已重命名属性的基础键,这可能会很棘手......有些人选择这样做而不是进行大量数据更改

假设你是从这个开始的:

class Person(ndb.Model):
   fname = ndb.StringProperty()
   lname = ndb.StringProperty()

有一天你真的决定改用 first_namelast_name 会更好......所以你这样做:

class Person(ndb.Model):
   first_name = ndb.StringProperty(name="fname")
   last_name = ndb.StringProperty(name="lname")

现在,当您执行 Person._properties(或 .properties() 或 person_instance._properties)时,您将获得一个字典,其中的键与基础名称(fname 和 lname)匹配...但与实际属性名称不匹配在类上...所以如果将它们放入新实例的构造函数中,或者使用 .populate() 方法将不起作用(上面的示例将中断)

无论如何,在 NDB 中,模型实例都有 ._values 字典,该字典由底层属性名称作为键......您可以直接更新它。我最终得到了这样的结果:

    def clone(entity, **extra_args):
        klass = entity.__class__
        clone = klass(**extra_args)
        original_values = dict((k,v) for k,v in entity._values.iteritems() if k not in clone._values)
        clone._values.update(original_values)
        return clone

这并不是最安全的方法...因为还有其他私有辅助方法可以完成更多工作(例如使用 _store_value()_retrieve_value 验证和转换计算属性())...但是如果你的模型足够简单,并且你喜欢生活在边缘:)

【讨论】:

【参考方案6】:

Nick 的回答启发了一个变体,它处理您的实体具有(重复)StructuredProperty 的情况,其中 StructuredProperty 本身具有 ComputedProperties。它可能可以通过某种方式用字典理解更简洁地编写,但这是对我有用的更长的版本:

def removeComputedProps(klass,oldDicc):
  dicc = 
  for key,propertType in klass._properties.iteritems():
      if type(propertType) is ndb.StructuredProperty:
          purged = []
          for item in oldDicc[key]:
              purged.append(removeComputedProps(propertType._modelclass,item))
          dicc[key]=purged
      else:
          if type(propertType) is not ndb.ComputedProperty:
              dicc[key] = oldDicc[key]
  return dicc

def cloneEntity(entity):
  oldDicc = entity.to_dict() 
  klass = entity.__class__
  dicc = removeComputedProps(klass,oldDicc)
  return klass(**dicc)

【讨论】:

【参考方案7】:

这是@zengabor 提供的code,其中if 表达式已格式化以便于阅读。它可能不符合 PEP-8:

klass = e.__class__
props = 
for k, v in klass.properties().iteritems():
    if not (type(v) == db.DateTimeProperty and ((
            skip_auto_now     and getattr(v, 'auto_now'    )) or (
            skip_auto_now_add and getattr(v, 'auto_now_add')))):
        if type(v) == db.ReferenceProperty:
            value = getattr(klass, k).get_value_for_datastore(e)
        else:
            value = v.__get__(e, klass)
        props[k] = value
props.update(extra_args)
return klass(**props)

【讨论】:

以上是关于在“编译”时不知道属性名称的情况下,在 Python 中复制 Google App Engine 数据存储区中的实体的主要内容,如果未能解决你的问题,请参考以下文章

试图在运行时不知道它们将是啥类型的情况下创建一个 DirectX 顶点数组

jQuery可排序更改名称属性在回发到php时不显示输入名称

VueJS:编译器错误状态数据属性在通过此属性访问时不存在

为啥在编译时不检查 lambda 返回类型?

属性存在于调试器中,但在编译时不存在

nmake / VC++ 2010:编译时不依赖 MSVCR100.dll