如何按照声明顺序读取类属性?

50

我正在编写一个元类,用于读取类属性并将它们存储在一个列表中,但我希望这个列表(cls.columns)能够保持声明的顺序(也就是说,在我的示例中应该是:mycol2mycol3zutcoolmenfina):

import inspect
import pprint

class Column(object):
    pass

class ListingMeta(type):
    def __new__(meta, classname, bases, classDict):
        cls = type.__new__(meta, classname, bases, classDict)
        cls.columns = inspect.getmembers(cls, lambda o: isinstance(o, Column)) 
        cls.nb_columns = len(cls.columns)
        return cls

class Listing(object):
    __metaclass__ = ListingMeta
    mycol2 = Column()
    mycol3 = Column()
    zut = Column()
    cool = Column()
    menfin = Column()
    a = Column()

pprint.pprint(Listing.columns)

结果:

[('a', <__main__.Column object at 0xb7449d2c>),
 ('cool', <__main__.Column object at 0xb7449aac>),
 ('menfin', <__main__.Column object at 0xb7449a8c>),
 ('mycol2', <__main__.Column object at 0xb73a3b4c>),
 ('mycol3', <__main__.Column object at 0xb744914c>),
 ('zut', <__main__.Column object at 0xb74490cc>)]

这不尊重Listing类的Column()属性声明顺序。如果我直接使用classDict,也没有帮助。

我该如何继续?


3
没有某种源码级别的分析,我认为你无法按顺序获取它们。无论如何,顺序应该大部分不相关。字典是按键哈希的,这就是为什么你看不到它按顺序排列的原因。 - Robert
总的来说,这是一个非常有建设性的问题。谢谢。 - pylover
你可以查看Tosca Widget 2,找到如何做到这一点。 - pylover
7个回答

40
在当前版本的Python中,类排序被保留。有关详细信息,请参见PEP520
在语言的旧版本(3.5及以下,但不包括2.x),您可以提供一个元类,该元类使用OrderedDict用于类命名空间。
import collections 

class OrderedClassMembers(type):
    @classmethod
    def __prepare__(self, name, bases):
        return collections.OrderedDict()

    def __new__(self, name, bases, classdict):
        classdict['__ordered__'] = [key for key in classdict.keys()
                if key not in ('__module__', '__qualname__')]
        return type.__new__(self, name, bases, classdict)

class Something(metaclass=OrderedClassMembers):
    A_CONSTANT = 1

    def first(self):
        ...

    def second(self):
        ...

print(Something.__ordered__)
# ['A_CONSTANT', 'first', 'second']

这种方法无法帮助您处理现有的类,您需要使用内省技术。

有什么想法或指针可以用来学习和了解introspection吗?(或者更好的是,在这种情况下如何应用它。) - not2qubit
需要注意的是,即使在P3.6中,使用外部类来帮助定义Something类项时,顺序也不总是保留。 - not2qubit

16

这是我刚开发的解决方法:

import inspect

class Column(object):
    creation_counter = 0
    def __init__(self):
        self.creation_order = Column.creation_counter
        Column.creation_counter+=1

class ListingMeta(type):
    def __new__(meta, classname, bases, classDict):
        cls = type.__new__(meta, classname, bases, classDict)
        cls.columns = sorted(inspect.getmembers(cls,lambda o:isinstance(o,Column)),key=lambda i:i[1].creation_order) 
        cls.nb_columns = len(cls.columns)
        return cls

class Listing(object):
    __metaclass__ = ListingMeta
    mycol2 = Column()
    mycol3 = Column()
    zut = Column()
    cool = Column()
    menfin = Column()
    a = Column()


for colname,col in Listing.columns:
    print colname,'=>',col.creation_order

起初我认为“你必须在每个类之后重置creation_counter”,然后我意识到你根本不需要这样做,只要你关心内部顺序。它实际上是可以工作的。 :) - Lennart Regebro
它如何在并行线程中工作?我认为这个代码不是线程安全的。 - pylover
对于Python3.6+,请参考@Conchylicultor的答案,这可能是基于语言中较新的字典顺序插入保证。您可以使用vars(cls)而不是更丑陋的cls.__dict__ - JL Peyret

16

从Python 3.6开始,这已经成为默认行为。请参见PEP520:https://www.python.org/dev/peps/pep-0520/

class OrderPreserved:
    a = 1
    b = 2
    def meth(self): pass

print(list(OrderPreserved.__dict__.keys()))
# ['__module__', 'a', 'b', 'meth', '__dict__', '__weakref__', '__doc__']

8

1) Python 3.6之后,类定义中的属性按照在源代码中出现的顺序进行排序,这个顺序现在被保存在新类的__dict__属性中(https://docs.python.org/3.6/whatsnew/3.6.html#whatsnew36-pep520):

class Column:
    pass

class MyClass:
    mycol2 = Column()
    mycol3 = Column()
    zut = Column()
    cool = Column()
    menfin = Column()
    a = Column()

print(MyClass.__dict__.keys())

您将会看到以下类似的输出(MyClass.__dict__ 可以像 OrderedDict 一样使用):
dict_keys(['__module__', 'mycol2', 'mycol3', 'zut', 'cool', 'menfin', 'a', '__dict__', '__weakref__', '__doc__'])

请注意,Python会添加额外的__xxx__字段,您可能需要忽略它们。
2) 对于之前的Python 3.x版本,您可以使用基于@Duncan答案的解决方案,但更简单。 我们利用__prepare__方法返回OrderDict而不是简单的dict这一事实 - 因此,在调用__new__之前收集的所有属性将被排序。
from collections import OrderedDict

class OrderedClass(type):
    @classmethod
    def __prepare__(mcs, name, bases): 
         return OrderedDict()

    def __new__(cls, name, bases, classdict):
        result = type.__new__(cls, name, bases, dict(classdict))
        result.__fields__ = list(classdict.keys())
        return result

class Column:
    pass

class MyClass(metaclass=OrderedClass):
    mycol2 = Column()
    mycol3 = Column()
    zut = Column()
    cool = Column()
    menfin = Column()
    a = Column()

现在您可以使用属性__fields__按所需顺序访问属性:
m = MyClass()
print(m.__fields__)
['__module__', '__qualname__', 'mycol2', 'mycol3', 'zut', 'cool', 'menfin', 'a']

请注意,type 类将生成属性 '__module__''__qualname__'。您可以按以下方式过滤名称来摆脱它们(修改OrderedClass.__new__):
def __new__(cls, name, bases, classdict):
    result = type.__new__(cls, name, bases, dict(classdict))
    exclude = set(dir(type))
    result.__fields__ = list(f for f in classdict.keys() if f not in exclude)
    return result    

它仅会返回MyClass类的属性:

['mycol2', 'mycol3', 'zut', 'cool', 'menfin', 'a']

3) 这个答案只适用于Python 3.x,因为Python 2.7中没有__prepare__定义。


6
如果您正在使用Python 2.x,则需要像Lennart提出的那样进行一些修改。如果您正在使用Python 3.x,则请阅读PEP 3115,因为其中包含一个示例,可以实现您想要的功能。只需修改示例以仅查看您的Column()实例即可。
 # The custom dictionary
 class member_table(dict):
    def __init__(self):
       self.member_names = []

    def __setitem__(self, key, value):
       # if the key is not already defined, add to the
       # list of keys.
       if key not in self:
          self.member_names.append(key)

       # Call superclass
       dict.__setitem__(self, key, value)

 # The metaclass
 class OrderedClass(type):

     # The prepare function
     @classmethod
     def __prepare__(metacls, name, bases): # No keywords in this case
        return member_table()

     # The metaclass invocation
     def __new__(cls, name, bases, classdict):
        # Note that we replace the classdict with a regular
        # dict before passing it to the superclass, so that we
        # don't continue to record member names after the class
        # has been created.
        result = type.__new__(cls, name, bases, dict(classdict))
        result.member_names = classdict.member_names
        return result

 class MyClass(metaclass=OrderedClass):
    # method1 goes in array element 0
    def method1(self):
       pass

    # method2 goes in array element 1
    def method2(self):
       pass

4

一种不包含方法的解决方案:

from collections import OrderedDict
from types import FunctionType


class StaticOrderHelper(type):
    # Requires python3.
    def __prepare__(name, bases, **kwargs):
        return OrderedDict()

    def __new__(mcls, name, bases, namespace, **kwargs):
        namespace['_field_order'] = [
                k
                for k, v in namespace.items()
                if not k.startswith('__') and not k.endswith('__')
                    and not isinstance(v, (FunctionType, classmethod, staticmethod))
        ]
        return type.__new__(mcls, name, bases, namespace, **kwargs)


class Person(metaclass=StaticOrderHelper):
    first_name = 'First Name'
    last_name = 'Last Name'
    phone_number = '000-000'

    @classmethod
    def classmethods_not_included(self):
        pass

    @staticmethod
    def staticmethods_not_included(self):
        pass

    def methods_not_included(self):
        pass


print(Person._field_order)

当使用类似 some_item = Field() 的方式在类中赋值时,导入的 Scrapy 项目也可以这样使用。具体可参考 imported Scrapy items - not2qubit

-2

我想你应该可以创建一个类,在其中用 ordered-dict 替换它的 __dict__


1
不错的想法。你测试过了吗? - pylover
我尝试了,但结果发现它不是按照“定义的顺序”。 - Nam G VU

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