通用属性


当你在Evennia中执行操作时,将数据存储起来以备后用往往很重要。如果要写一个菜单系统,你需要跟踪菜单的当前位置,这样玩家才能给出正确的后续命令。如果你在编写战斗系统,你可能想要这样设计:如果战斗一方的动作失败了,另一方的动作会更容易命中。你的角色可能需要存储一些角色扮演的属性,如力量、敏捷等。

类型类的实例(玩家物体脚本频道)都可以使用通用属性。通用属性可以用来存储这些实例“上”的任何类型的数据。这和将数据保存在实例的特定属性(如 key 或 location)中不同,这些属性都有特定的名字,只能存储特定类型的数据(比如不论你如何努力都无法将 Python 的列表存入属性 key 中)。如果你想用任意名字存储任意数据,通用属性就可以派上用处。


保存及取回数据

为了将数据持久存储在类型类对象中,你可以使用 db(数据库)操作。让我们将一些数据保存到 Rose(物体)中:
    # 存储
    rose.db.has_thorns = True
    # 取回
    is_ouch = rose.db.has_thorns
这看起来就像 Python 的普通赋值,但 db 会确保在幕后创建一个通用属性,并将它存储到数据库中。现在,在整个服务器存续期间你的玫瑰都会带着刺了,除非你故意删除它。

如果要非持久保存,即不创建数据库条目,可以使用 ndb(非数据库)。它的工作方式类似:
    # 存储
    rose.ndb.has_thorns = True
    # 取回
    is_ouch = rose.ndb.has_thorns
严格地说,尽管它们看起来类似,但 ndb 是和通用属性无关的。当使用 ndb 时不会在幕后创建通用属性对象。因为我们不需要持久性,所以根本不会调用数据库。

你还可以删除 db 和 ndb 中的属性。以下的例子会删除一个属性:
    del rose.db.has_thorns
db 和 ndb 都默认提供 all() 方法。这会返回所有已保存的通用属性或非持久性属性。
     list_of_all_rose_attributes = rose.db.all()
     list_of_all_rose_ndb_attrs = rose.ndb.all()
如果你已将 all 作为某个属性的名称,则以上的功能会被替换。在你删除自定义的 all 属性之后又会恢复默认的行为。

你也可以通过通用属性处理程序 attributes 访问通用属性,所有类型类对象上都可以使用它(如rose.attributes.get())。比如,你可以通过它访问在运行中动态生成的属性。请查阅源代码了解可用的参数。
  • attributes.has(...) —— 用来检查对象是否拥有指定的通用属性。相当于执行 obj.db.key。
  • get(...) —— 取得指定的通用属性。通常是返回属性的值,但该方法也可以通过设定关键字参数返回属性对象本身。如果向该方法提供 accessing_obj 参数,则可以确保在作修改之前进行权限检查。
  • add(...) —— 给对象添加新的通用属性。可以提供一个可选的锁字符串,用来限制今后的访问,就连这个方法本身也要做访问检查。
  • remove(...) —— 删除指定的属性。可以选择在删除之前先进行权限检查。
  • clear(...) —— 删除对象的所有属性。
  • all(...) —— 返回对象上(指定类别)的所有属性。
关于如何锁定访问、编辑通用属性的信息,请参阅之后的内容。

相应的,还有一个 nattribute 用于管理非数据库属性。它拥有相同的方法,但工作方式要简单得多,因为它不管类别、不管字符串值,也不提供访问权限控制。


通用属性的属性

通用属性对象是存储在数据库中的,它具有以下属性:
  • key —— 通用属性的名称。比如执行 obj.db.attrname = value,attrname 会赋给 key。
  • value —— 通用属性的值。值可以是任何能够 pickle 的东西,如对象、列表、数字或其他东西(更多信息请参阅后面的内容)。在例子 obj.db.attrname = value 中,value就存储在这里。
  • category —— 这是可选的,在大多数通用属性中为 None。设置它可以让通用属性具有不同的功能。通常你不需要这样做,除非你想用将通用属性用于完全不同的用途(比如昵称就是这样的一个例子)。你必须使用属性处理程序来修改这个值。
  • strvalue —— 这是一个独立的字段,只接受字符串。它会严重限制可存储的数据,但便于数据库的快速搜索。通常不会使用这个值,除非需要将基本属性用于其他目的(昵称会使用它)。只能属性处理程序来访问它。

此外,还有两个特殊属性:
  • attrtype —— Evennia会在内部使用它,用于将昵称从基本属性中区分出来(昵称会在幕后使用通用属性)。
  • model —— 这是一个自然关键字,用来描述该属性附加到的模块,形式为“包名.模块类”,比如 objects.objectdb。属性和昵称处理程序将它用于数据库快速排序匹配。它和 attrtype 通常不需要修改。

非数据库属性没有 category、strvalue、attrtype及model。


持久性与非持久性

持久性意味着你的数据不会因服务器重启而丢失,而非持久性数据则会丢失...

那...为什么还要用非持久性数据呢?答案是:你不需要持久性。大多数时候,你的确想保存尽可能多的数据,但非持久性数据可能会在某些情况下有用。
  • 你会担心数据库的性能。由于Evennia会非常积极地缓存通用属性,因此这通常不是什么问题,除非你频繁地读写基本属性(比如每秒很多次)。读取已缓存的基本属性的速度就和读取任何 Python 属性一样快。就算不这样,你也没什么可担心的:除了Evennia自己的缓存,现代的数据库系统本身也会为提高速度而高效率地缓存数据。如果允许,我们的默认数据库甚至可以完全在内存中运行,在面对高负载时这可以减少大量的磁盘写入。
  • 使用非持久性数据还有个更显著的理由,那就是你想在注销时主动丢弃一些数据。也许你存储了一些在服务器启动时会重新初始化的数据,也许你自己实现了一部分缓存,或者也许你正在测试一个有bug的脚本,它可能会对你的角色对象产生危害。使用非持久性存储可以保证即使你弄得一团糟,也能够通过重启服务器把一切清理干净。
  • 你自己想创建一个完全非持久或部分非持久的世界。我们不会与你的美好愿景争辩!


哪些类型的数据可以保存在通用属性中?

通用属性使用 pickle 模块将数据序列化,然后存储到数据库中(除非你在属性处理程序中使用 strattr 关键字,将数据存入 strvalue 中。在这种情况下,你只能保存字符串)。如果你存储是一个对象(不是对象的可迭代列表),实际上你可以存储任何可 pickle 的Python对象。

但是如果你想存储多个对象,只能将它们放在 Python 的内置结构类型中(即元组、列表、字典或集合)。所有其他的迭代类型(如自定义容器)都可能要转成列表(这样做的原因见下一节)。由于字典、集合、列表和元组可以随意组合嵌套,因此不会给你带来多少限制。

要注意,有一种类型的对象是不能被 pickle 的,这就是 Django 数据库对象。存储时它会被转存为一个包含id和数据库模型的包装对象,而访问通用属性时它会被读取为一个新的类型类实例。如果数据库对象没有正确地存入基本属性会导致错误发生,因此Evennia会分析存入的数据,查找其中的数据库对象。Evennia必须递归遍历所有的可迭代对象,以确保其中所有的数据库对象都被安全存储。因此如果可以的话,为了提高效率应该避免使用深层嵌套的对象列表。

需要注意的是,你是可以骗过安全检查的,比如创建自定义的非迭代类,然后将数据库对象存入其中。因此需要明确一点:不支持用这种方式保存对象,这可能会让你的游戏不稳定。使用列表、元组、字典、集合或它们的组合存储数据库对象,这样就不会有问题了。
    # 有效属性数据的例子:
    # 单个的值
    obj.db.test1 = 23
    obj.db.test1 = False 
    # 一个数据库对象(会被存储为dbref)
    obj.db.test2 = myobj
    # 对象列表
    obj.db.test3 = [obj1, 45, obj2, 67]
    # 字典
    obj.db.test4 = {'str':34, 'dex':56, 'agi':22, 'int':77}
    # 混合字典和列表
    obj.db.test5 = {'members': [obj1,obj2,obj3], 'enemies':[obj4,obj5]}
    # 包含了列表的元组
    obj.db.test6 = (1,3,4,8, ["test", "test2"], 9)
    # 存储的是集合,但返回的是列表 [1,2,3,4,5]!
    obj.db.test7 = set([1,2,3,4,5])
    # 原处修改
    obj.db.test8 = [1,2,{"test":1}]
    obj.db.test8[0] = 4
    obj.db.test8[2]["test"] = 5
    # test8 现在是 [4,2,{"test":5}]
不支持的保存方式的例子
    # 这会欺骗数据库对象检测,因为 myobj (数据库对象) 被“隐藏”
    # 在用户对象中了。支持这种做法,因为可能会导致无法预料的后果。
    class BadStorage(object):
        pass
    bad = BadStorage()
    bad.dbobj = myobj
    obj.db.test8 = bad # 这可能会导致回朔


取出可变对象

Evennia存储通用属性的方式有个副作用,Python的列表、字典和集合需要由名为 PackedLists、PackedDicts和PackedSets 的自定义对象来处理。它们的行为就像普通的列表、字典一样,但它们也有特殊之处,每当它们的数据发生变化,它们都会向数据库保存自己。这个功能可以让你直接执行 self.db.mylist[7] = val,而不需要将 mylist 先取出来放到临时变量中。

然而,有件很重要的事需要记住。如果你将这个数据取到另一个变量中,如 mylist2 = obj.db.mylist,你的新变量 mylist2 仍然是一个 PackedList 对象!因此每当它发生变化,它会继续将自己保存到数据库中!这点需要牢记,不然你会被结果弄糊涂的。
    obj.db.mylist = [1,2,3,4]
    mylist = obj.db.mylist
    mylist[3] = 5 # 它仍会更新数据库
    print mylist # 现在是 [1,2,3,5]
    print mylist.db.mylist # 现在也是 [1,2,3,5]
想让取出来的变量与数据库“断开连接”,只需要简单地将 PackedList 或 PackedDict 转为Python的普通列表或字典就行了。这可以通过内建的 list() 和 dict() 函数来实现。如果遇到“嵌套”的列表或字典,你只需要转换“最外层”的列表或字典,就可以切断整个结构体与数据库的连接。
    obj.db.mylist = [1,2,3,4]
    mylist = list(obj.db.mylist) # 转换为普通列表
    mylist[3] = 5
    print mylist # 现在是 [1,2,3,5]
    print obj.db.mylist # 现在仍是 [1,2,3,4]
请记住,这只适用于可变对象 —— 列表、字典及它们的组合。不可变对象(字符串、数字、元组等)一开始就和数据库断开连接了。因此,将最外层的迭代器转为元组也是阻止结构体更新数据库的一个办法。
    obj.db.mytup = (1,2,[3,4])
    obj.db.mytup[0] = 5 # 失败,因为元组是不可变的
    obj.db.mytup[2][1] = 5 # 这个可以执行但不会更新数据库,因为最外层的迭代器是一个元组
    print obj.db.mytup[2][1] # 这仍然会返回4而不是5
    mytup1 = obj.db.mytup
    # mytup1已从数据库断开,因为最外层的迭代对象是元组,
    # 所以我们可以随意修改其内部的列表而不会影响到数据库。


锁定及检查通用属性

在默认情况下属性通常是不会被锁定的,但对于单个属性你可以很容易地改变这一点(比如在一些对用户等级敏感的游戏中)。

首先,你需要设置通用属性的锁字符串。锁字符串是特定的。相关的锁类型如下:
  • attrread —— 限定哪些人可以读取这个通用属性的值
  • attredit —— 限定哪些人可以设定/更改这个通用属性

你不能用数据库处理程序来修改通用属性对象(比如给它们设置锁),数据库处理程序会返回通用属性的值,而不是通用属性对象本身。你应该使用通用属性处理程序,通过设置参数让它返回对象而不是值:
    lockstring = "attread:all();attredit:perm(Wizards)"
    obj.attributes.get("myattr", return_obj=True).locks.add(lockstring)
请注意 return_obj 关键字,它可以确保返回的是通用属性对象,这样就可以访问它的锁处理程序了。

如果没人检查锁,那即使锁了也没用,Evennia默认是不会检查属性上的锁的。你需要在命令代码的合适地方(比如设置通用属性之前)添加检查。
    # 在某个命令的代码中,在我们想限制
    # 设置对象上某个通用属性的地方
    attr = obj.attributes.get(attrname, 
                              return_obj=True, 
                              accessing_obj=caller, 
                              default=None, 
                              default_access=False)
    if not attr: 
        caller.msg("你不能修改这个属性!")
        return
    # 在这里修改属性
相同的关键字也可以用在 obj.attributes.set() 和 obj.attributes.remove() 上,它们会检查 attredit 的锁类型。


(原文:https://github.com/evennia/evennia/wiki/Attributes    翻译:卢铱俊)