Как я могу получить поля в исходном порядке?

У меня есть код вроде:

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

print(dir(Ordered))

он печатает:

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

Как я могу получить поля в исходном порядке: x, z, b, a? Я видел подобное поведение в моделях Django.


person DenisKolodin    schedule 20.07.2010    source источник
comment
Методы, свойства и т. д. хранятся как словари внутри объектов, а словари по определению неупорядочены. Чего вы пытаетесь достичь (или, лучше сказать, почему)?   -  person Tim Pietzcker    schedule 20.07.2010
comment
Вам нужен Python 3, чтобы сделать это без хаков: stackoverflow.com/a/4460565/821378   -  person Éric Araujo    schedule 28.02.2013


Ответы (5)


Как упоминалось выше, если вы хотите, чтобы все было просто, просто используйте, например, атрибут _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)]

Не стесняйтесь изменить это на упорядоченный dict или просто вернуть имена или все, что вам нужно. Кроме того, вы можете определить пустой класс с помощью __metaclass__ и наследовать его.

Не используйте это!

Как видите, этот подход немного усложнен и, вероятно, не подходит для большинства задач или разработчиков Python. Если вы новичок в python, вы, вероятно, потратите больше времени и усилий на разработку своего метакласса, чем если бы вы просто определили порядок вручную. Определение собственного порядка вручную почти всегда будет лучшим подходом. Django делает это автоматически, потому что сложный код скрыт от конечного разработчика, а Django используется гораздо чаще, чем сам пишется/поддерживается. Так что метаклассы могут быть вам полезны только в том случае, если вы разрабатываете фреймворк для других разработчиков.

person Will Hardy    schedule 20.07.2010
comment
Я разрабатываю фреймворк для трейдеров и ваше решение, то что мне нужно. Я хочу упростить объявление входных и выходных параметров для индикаторов, и я действительно использую экземпляры в качестве полей. С int у меня слишком упрощенный пример))) Спасибо за лучший ответ! :) - person DenisKolodin; 20.07.2010
comment
ПРЕДУПРЕЖДЕНИЕ! Эта версия new не получает поля из баз. Поэтому подклассы теряют упорядоченные поля родителя! Добавьте их, если это необходимо. - person DenisKolodin; 05.10.2010

Я был готов на 80% с этим ответом, когда Уилл опубликовал свой, но я все равно решил опубликовать, чтобы усилия не пропали даром (наши ответы в основном описывают одно и то же).

Вот как это делает Джанго. Я решил сохранить ту же номенклатуру, методологию и структуры данных, что и 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']

Надеюсь, это поможет прояснить все, что еще не было ясно в ответе Уилла.

person Aram Dulyan    schedule 20.07.2010
comment
Ваше решение должно быть обновлено до python 3.x, где cmp(), похоже, больше нет. Определите __lt__(), а не __cmp__(). В остальном спасибо! - person Fabio A.; 04.04.2013
comment
И определение метакласса должно быть сделано так: class Model(metaclass=ModelBase). - person Fabio A.; 04.04.2013

Модель и метаклассы формы Django работают вместе с дескрипторами полей для поддержания исходного порядка. Невозможно выполнить то, о чем вы просите, не преодолевая много препятствий. Посмотрите исходный код Django, если вам все еще интересно.

person Ignacio Vazquez-Abrams    schedule 20.07.2010

Вы не можете отслеживать порядок добавления переменных класса. Эти атрибуты (а также атрибуты объектов) хранятся внутри в виде словаря, который оптимизирован для быстрого поиска и не поддерживает упорядочение.

Вы можете увидеть этот факт:

class A(object):
    x = 0
    y = 0
    z = 0

A.__dict__.items()

#  [('__module__', '__main__'), 
#   ('__dict__', <attribute '__dict__' of 'A' objects>), 
#   ('y', 0), ('x', 0), ('z', 0), 
#   ('__weakref__', <attribute '__weakref__' of 'A' objects>), 
#   ('__doc__', None)]

Если вы хотите, чтобы ваши атрибуты располагались в определенном порядке, вы можете просто добавить еще одно поле, содержащее эту информацию:

class B(object):
    x = 0
    y = 0
    z = 0
    a = 0
    _ordering = ['x', 'y', 'z', 'a']

print B._ordering
# => ['x', 'y', 'z', 'a']

Примечание: в Python 2.7 и 3.2 упорядоченные словари будут представлены как часть стандартной библиотеки.

person miku    schedule 20.07.2010
comment
Это неплохо, но не соответствует принципу DRY. - person DenisKolodin; 20.07.2010

Пока просто используйте Python 3.6!

class OrderedClass():
    x = 0
    z = 0
    a = 0
    b = 0

print(list(OrderedClass.__dict__))

Это выводит меня:

['__module__', 'x', 'z', 'a', 'b', '__dict__', '__weakref__', '__doc__']

person Artem Selivanov    schedule 26.10.2016
comment
Это потому, что PEP 520 - person DenisKolodin; 26.10.2016