如何按原始顺序获取字段?

9 投票
6 回答
3825 浏览
提问于 2025-04-16 01:33

我有一段代码:

class Ordered(object):
    x = 0
    z = 0
    b = 0
    a = 0

print(dir(Ordered))

它输出:

[ ......., a, b, x, z]

我该如何让字段按照原来的顺序:x, z, b, a 来显示呢?我在Django模型中也见过类似的情况。

6 个回答

3

在编程中,我们经常会遇到一些问题,特别是在使用某些工具或库的时候。比如,有时候你可能会发现你的代码运行得不如预期,或者出现了一些错误。这时候,很多人会选择去网上查找解决方案,比如在StackOverflow上提问或者寻找别人遇到过的类似问题。

在提问的时候,清楚地描述你的问题是非常重要的。你需要告诉别人你在做什么,遇到了什么具体的错误信息,以及你已经尝试过哪些解决方法。这样,别人才能更好地理解你的问题,并给出有效的建议。

另外,分享你的代码也是很有帮助的。这样别人可以直接看到你是如何实现的,可能会更容易发现问题所在。记得在分享代码时,使用代码块格式,这样可以让代码看起来更整洁,阅读起来也更方便。

总之,提问时要尽量详细和清晰,这样才能得到更好的帮助。

class SchemaItem():
    def __init__(self,item):
        self.item = item
        time.sleep(0.1)
        self.order = datetime.now()

    def __str__(self):
        return "Item = %s, Order = %s"%(self.item, self.order)

class DefiningClass():
    B       = SchemaItem("B")
    C       = SchemaItem("C")
    A       = SchemaItem("A")
    PRODUCT = SchemaItem("PRODUCT")
    ASSSET  = SchemaItem("ASSET")
    TENOR   = SchemaItem("TENOR")

    def get_schema(self):
        self_class = self.__class__
        attributes = [x for x in dir(self_class) if x not in ["class","name","schema","values"]]
        schema     = [(attribute_name,getattr(self_class,attribute_name)) for attribute_name in attributes if isinstance(getattr(self_class,attribute_name),SchemaItem)]
        return dict(schema)

# Actual usage
ss = [(name,schema_item) for name,schema_item in s.get_schema().items()]
print "Before = %s" % ss
ss.sort(key=lambda a:a[1].order)
print "After =%s" % ss
5

我在Will发布他的回答时,已经写了80%的内容,但我还是决定发出来,这样我的努力就不会白费(我们的回答基本上描述的是同样的内容)。

下面是Django是怎么做的。我选择保持与Django相同的命名、方法和数据结构,这样这个回答也可以帮助那些想理解Django中字段名称是如何排序的人。

from bisect import bisect

class Field(object):
    # A global creation counter that will contain the number of Field objects
    # created globally.
    creation_counter = 0

    def __init__(self, *args, **kwargs):
        super(Field, self).__init__(*args, **kwargs)
        # Store the creation index in the "creation_counter" of the field.
        self.creation_counter = Field.creation_counter
        # Increment the global counter.
        Field.creation_counter += 1
        # As with Django, we'll be storing the name of the model property
        # that holds this field in "name".
        self.name = None

    def __cmp__(self, other):
        # This specifies that fields should be compared based on their creation
        # counters, allowing sorted lists to be built using bisect.
        return cmp(self.creation_counter, other.creation_counter)

# A metaclass used by all Models
class ModelBase(type):
    def __new__(cls, name, bases, attrs):
        klass = super(ModelBase, cls).__new__(cls, name, bases, attrs)
        fields = []
        # Add all fields defined for the model into "fields".
        for key, value in attrs.items():
            if isinstance(value, Field):
                # Store the name of the model property.
                value.name = key
                # This ensures the list is sorted based on the creation order
                fields.insert(bisect(fields, value), value)
        # In Django, "_meta" is an "Options" object and contains both a
        # "local_fields" and a "many_to_many_fields" property. We'll use a
        # dictionary with a "fields" key to keep things simple.
        klass._meta = { 'fields': fields }
        return klass

class Model(object):
    __metaclass__ = ModelBase

现在我们来定义一些示例模型:

class Model1(Model):
    a = Field()
    b = Field()
    c = Field()
    z = Field()

class Model2(Model):
    c = Field()
    z = Field()
    b = Field()
    a = Field()

接下来我们来测试一下这些模型:

>>>> [f.name for f in Model1()._meta['fields']]
['a', 'b', 'c', 'z']
>>>> [f.name for f in Model2()._meta['fields']]
['c', 'z', 'b', 'a']

希望这能帮助澄清Will的回答中没有说明的内容。

15

如上所述,如果你想简单点,可以直接使用一个 _ordering 属性,手动记录顺序。否则,这里有一种元类的方法(就像Django使用的那样),可以自动创建一个顺序属性。

记录原始顺序

类本身并不会记录属性的顺序。不过,你可以跟踪字段实例创建的顺序。为此,你需要为字段使用自己的类(而不是直接用int)。这个类会记录已经创建了多少个实例,每个实例会记下自己的位置。下面是如何为你的例子(存储整数)做到这一点:

class MyOrderedField(int):
  creation_counter = 0

  def __init__(self, val):
    # Set the instance's counter, to keep track of ordering
    self.creation_counter = MyOrderedField.creation_counter
    # Increment the class's counter for future instances
    MyOrderedField.creation_counter += 1

自动创建一个 ordered_items 属性

现在你的字段有了一个可以用来排序的数字,你的父类需要以某种方式使用这个数字。你可以用多种方法来实现,如果我没记错的话,Django就是用元类来做到这一点,这对一个简单的类来说有点复杂。

class BaseWithOrderedFields(type):
  """ Metaclass, which provides an attribute "ordered_fields", being an ordered
      list of class attributes that have a "creation_counter" attribute. """

  def __new__(cls, name, bases, attrs):
    new_class = super(BaseWithOrderedFields, cls).__new__(cls, name, bases, attrs)
    # Add an attribute to access ordered, orderable fields
    new_class._ordered_items = [(name, attrs.pop(name)) for name, obj in attrs.items()
                                    if hasattr(obj, "creation_counter")]
    new_class._ordered_items.sort(key=lambda item: item[1].creation_counter)
    return new_class

使用这个元类

那么,怎么使用这个呢?首先,在定义属性时,你需要使用我们新的 MyOrderedField 类。这个新类会跟踪字段创建的顺序:

class Ordered(object):
  __metaclass__ = BaseWithOrderedFields

  x = MyOrderedField(0)
  z = MyOrderedField(0)
  b = MyOrderedField(0)
  a = MyOrderedField(0)

然后你可以在我们自动创建的属性 ordered_fields 中访问这些有序字段:

>>> ordered = Ordered()
>>> ordered.ordered_fields
[('x', 0), ('z', 0), ('b', 0), ('a', 0)]

你可以随意将其更改为有序字典,或者只返回名称,或者根据需要进行调整。此外,你可以定义一个空类,并使用 __metaclass__ 进行继承。

不要使用这个!

如你所见,这种方法有点复杂,可能不适合大多数任务或Python开发者。如果你对Python还不太熟悉,你可能会花更多的时间和精力在开发元类上,而不是手动定义顺序。手动定义顺序几乎总是最佳选择。Django之所以能自动处理,是因为复杂的代码对最终开发者是隐藏的,而且Django的使用频率远高于它的编写和维护。因此,只有在你为其他开发者开发框架时,元类才可能对你有用。

撰写回答