【问题标题】:Forward declaration of classes?类的前向声明?
【发布时间】:2011-05-08 22:15:33
【问题描述】:

我有一些类看起来像这样:

class Base:
  subs = [Sub3,Sub1]
  # Note that this is NOT a list of all subclasses!
  # Order is also important

class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass
...

现在,这会失败,因为在 Base.subs 定义时未定义 Sub1 和 Sub3。但显然我也不能将子类放在 Base 之前。有没有办法在 Python 中前向声明类?我想使用isinstance,所以 subs 中的类型实际上必须与后面声明的子类相同,它们具有相同的名称和其他属性是不够的。

一种解决方法是:Base.subs = [Sub3,Sub1]定义了子类之后,但我不喜欢这样拆分我的类。

编辑:添加有关订单的信息

【问题讨论】:

  • 这是一个糟糕的设计。您将工厂(知道子类)与超类(不需要知道子类)混为一谈。为什么要这样做?为什么不简单地把事情分开,让事情变得更简单?
  • 感谢您的帮助,我不想显得忘恩负义,但我不禁想知道为什么 SO 上的人们似乎花这么多时间思考问题背后的动机和原因,而不仅仅是回答问题。毕竟这是一个相当简单的问题。
  • @pafcu:如果你开始走错了路,写出了糟糕的设计代码,我们真的帮不了你,只能说,停止那样做,走上一条不适合你的路有明显的问题。这不是一个“直截了当的问题”。如果是这样,您将自己找到答案。这个问题反映了一个常见的设计错误。改正错误,你就不用再问这个问题了。
  • @martineau:这是一个开发者社区,对吧?这就像一个医生向另一个医生要药,通常他不会被第二次猜测,除非他明显错了。我有理由问这个具体的问题。由于我提供的信息有限,原因可能不清楚,但它仍然存在。当我的问题有效时,没有理由相信 no 的情况。无论如何,这已经够离题了。
  • @martineau:是的,我理解人们很好奇(我也经常这样,这是一种很好的学习方式),但 SO 是一个问答网站,而且(恕我直言)不是为了深入讨论设计选择(除非问题是专门针对这个的)。我为听起来咄咄逼人而道歉,但似乎当我在这里提出问题时,我最终会花费更多时间来解释我的设计选择,而不是我自己解决问题的时间。

标签: python


【解决方案1】:

编辑:由于增加了订单要求,我完全修改了我的答案。我还使用了一个类装饰器,这里首先由@Ignacio Vazquez-Abrams 使用。

Edit2: 代码现已经过测试,一些愚蠢的问题稍作纠正


class Base(object):
    subs = []

    @classmethod
    def addsub(cls, before=None): 
        def inner(subclass):
            if before and before in cls.subs:
                cls.subs.insert(cls.subs.index(before), subclass)
            else:
                cls.subs.append(subclass)
            return subclass
        return inner

@Base.addsub()
class Sub1(Base):
    pass

class Sub2(Base):
    pass

@Base.addsub(before=Sub1)
class Sub3(Base):
    pass

【讨论】:

  • 在这种情况下,很难确定 Base.subs 列表的顺序。这取决于我编写代码的顺序,这很容易出错。此外,Base.subs 是 Base 类的属性,所以我更愿意在其中定义它,而不是将其分散到多个类中。
  • 是的,如果有人没有意识到类顺序很重要(通常不会),代码会以有趣的方式中断。例如,很容易意外更改订单。重构或清理代码。
  • 我还没有看到应该重要的问题。也许您应该考虑 a) 在生成和维护顺序方面更加明确,或者 b) 完全不依赖于顺序。为什么重要?
  • 另外,Sub.__new__ 影响的是实例创建,而不是这里需要的类创建。
  • 我喜欢这个答案,因为关于子类的信息没有重复,而且基类不知道它的子类,这很好。是否以及在何处将新子类添加到优先级列表中的决定取决于其实现者,并且在一个地方。一个缺点是“之前”施加的顺序是部分的,因此结果取决于编写(或处理)类的顺序
【解决方案2】:

编写一个装饰器,将其添加到Base 中的注册表中。

class Base(object):
  subs = []

  @classmethod
  def addsub(cls, scls):
    cls.subs.append(scls)

 ...

@Base.addsub
class Sub1(Base):
  pass

class Sub2(Base):
  pass

@Base.addsub
class Sub3(Base):
  pass

【讨论】:

  • 抱歉,忘记指定顺序很重要(这是“首选”类型的列表)。这取决于代码的顺序是非常危险的。我想我可以为此添加一个参数到 addsub 。这仍然存在问题,尽管 Base.subs 的内容是到处定义的。
【解决方案3】:
class Foo:
  pass

class Bar:
  pass

Foo.m = Bar()
Bar.m = Foo()

【讨论】:

  • 这在问题中被排除了。 Foo的定义是分裂的。
【解决方案4】:

我只是将子类定义为字符串,并让不可避免的装饰器将字符串替换为它们命名的类。我还会在元类上定义装饰器,因为我认为这更符合目标:我们正在修改类行为,就像您通过修改其类来修改对象行为一样,您可以通过修改其元类来修改类行为。

class BaseMeta(type):

    def register(cls, subcls):
        try:
            i = cls.subs.index(subcls.__name__)
        except ValueError:
            pass
        else:
            cls.subs[i] = subcls
        finally:
            return cls


class Base(object):
    __metaclass__ = BaseMeta
    subs = ['Sub3', 'Sub1']

@Base.register
class Sub1(Base): pass

@Base.register
class Sub2(Base): pass

@Base.register
class Sub3(Base): pass

print Base.subs

这个输出:

[<class '__main__.Sub3'>, <class '__main__.Sub1'>]

【讨论】:

  • 这似乎是一种非常有趣的方法,因为我可以在一个地方指定 base.subs 的所有元素。如果需要,可以更轻松地进行更改。
  • @martineau,关键是我们正在改变类的行为。通过更改类本身来做到这一点本质上是猴子修补对象。改变一个类的原因是改变它的实例的行为——而不是改变它自己的行为。你可以通过改变 it 是一个实例的类,即它的元类来做到这一点。我们不再使用 C++ 编程,类也是对象 :) 另外,我删除了对您的回答的尖刻评论。出于某种原因,我实际上并没有看到针对我的 cmets。
  • @aaronasterling:感谢您的解释。虽然我理解并且通常会完全同意您的理由,但我认为它在这里并不真正适用,因为将子类装饰器粘贴到元类中不会有任何效果,除非它使用 @ 语法显式应用于子类。换句话说,在这种情况下,将它放在元类中似乎是一种不必要且可能令人困惑的并发症——这也是为什么我有意识地选择在我自己的答案中将其作为 Base 类的类方法。
  • @martineau,好吧,我想我从来没有写过让懒惰的人能理解的代码——如果他们能做得很好,或者做得不好。所以除了混乱的问题,我的观点是站得住脚的。类方法只是用漂亮的语法修补猴子。改变元类是正确的方法。
  • @aaronasterling:嗯,我当然不认为我对这些事情很懒惰,所以虽然我可以很容易理解如何你的代码是如何工作的,但我不确定为什么你这样做(所以我问)。从您写的内容看来,您认为 A. 所有类方法都是猴子补丁(因此通常是一种糟糕的做事方式),并且 B. 所有类装饰器本质上都是只是类方法,应该放在一个元类中。正确的?我也忍不住想知道“总是做“最简单的事情可能起作用”这句格言是如何适应这一切的......
【解决方案5】:

这基本上是@Ignacio Vazquez-Abrams' 和@aaronasterling 的答案的混合版本,它保留了列表中子类的顺序。最初所需的子类名称(即字符串)以所需的顺序手动放置在subs 列表中,然后在定义每个子类时,类装饰器会导致相应的字符串替换为实际的子类:

class Base(object):  # New-style class (i.e. explicitly derived from object).

    @classmethod
    def register_subclass(cls, subclass):
        """ Class decorator for registering subclasses. """

        # Replace any occurrences of the class name in the class' subs list.
        # with the class itself.
        # Assumes the classes in the list are all subclasses of this one.
        # Works because class decorators are called *after* the decorated class'
        # initial creation.
        while subclass.__name__ in cls.subs:
            cls.subs[cls.subs.index(subclass.__name__)] = subclass

        return cls  # Return modified class.

    subs = ['Sub3', 'Sub1']  # Ordered list of subclass names.


@Base.register_subclass
class Sub1(Base): pass

@Base.register_subclass
class Sub2(Base): pass

@Base.register_subclass
class Sub3(Base): pass

print('Base.subs: {}'.format(Base.subs))
# Base.subs: [<class '__main__.Sub3'>, <class '__main__.Sub1'>]

更新:元类

同样的事情也可以使用元类来完成——它的优点是它消除了显式装饰每个子类的需要,如我上面的原始答案所示(你接受了),但是它使这一切自动发生。

请注意,即使为创建 每个 子类调用元类的 __init__(),它也只会在子类的名称出现在其中时更新 subs 列表——所以初始 Base subs 列表的内容的 class' 定义仍然控制其中被替换的内容(保持其顺序)。

class BaseMeta(type):

    def __init__(cls, name, bases, classdict):
        if classdict.get('__metaclass__') is not BaseMeta:  # Metaclass instance?
            # Replace any occurrences of a subclass' name in the class being
            # created the class' sub list with the subclass itself.
            # Names of classes which aren't direct subclasses will be ignored.
            while name in cls.subs:
                cls.subs[cls.subs.index(name)] = cls

        # Chain to __init__() of the class instance being created after changes.
        # Note class instance being defined must be new-style class.
        super(BaseMeta, cls).__init__(name, bases, classdict)


# Python 2 metaclass syntax.
class Base(object):  # New-style class (derived from built-in object class).
    __metaclass__ = BaseMeta
    subs = ['Sub3', 'Sub1']  # Ordered list of subclass names.

# Python 3 metaclass syntax.
#class Base(metaclass=BaseMeta):
#    subs = ['Sub3', 'Sub1']  # Ordered list of subclass names.


# Note: No need to manually register the (direct) subclasses.
class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass

print('Base.subs: {}'.format(Base.subs))

输出:

Base.subs: [<class '__main__.Sub3'>, <class '__main__.Sub1'>]

请务必注意,这两个答案之间至少存在一个细微差别——即第一个将与通过 @Base.register_subclass() 注册的 any 类名一起使用,无论它是否真的是Base 的子类(尽管这可能会更改/修复。)

我指出这一点有几个原因:首先,因为在您的 cmets 中,您说 subs 是“列表中的一组类,其中一些可能是它的子类”,更重要的是,因为不是我更新中的代码的情况,它仅适用于Base 子类,因为它们有效地通过元类自动“注册”——但会将其他任何内容单独留在列表中。这可以被认为是一个错误或一个功能。 ;¬)

更新:Python 3.6+

在 Python 3.6 中添加了一个名为 __init_subclass__() 的新 object 方法,它提供了一种更简单的方法来实现也不需要装饰所有子类或定义元类:

#!/usr/bin/env python3.6

class Base:
    subs = ['Sub3', 'Sub1']  # Ordered list of subclass names.

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        while cls.__name__ in cls.subs:
            cls.subs[cls.subs.index(cls.__name__)] = cls


# Note: No need to manually register the subclasses.
class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass

print('Base.subs: {}'.format(Base.subs))

【讨论】:

  • 很好的答案。我徘徊在“子类”不是时它是否有效,即不是从基类派生的。这似乎是一个非常有用的用例,其中有一个独立类列表和一个聚合器/管理器类
  • @davka: 是的,它可以用于那个目的——至少第一个 sn-p 中的代码可以,因为它不关心通过Base.register_subclass() 装饰器注册的类是否实际上是Base 类的子类与否。但是,在更新代码的情况下,只有基类 subs 列表中的实际子类名称将被相应的子类替换(假设定义了具有给定名称的子类),因为它没有提供强制执行的方法发生(例如通过装饰器调用)。
【解决方案6】:

我很确定这对你有用。之后只需分配依赖的类属性。 这也简单得多。

class Base:pass

class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass

Base.subs = [Sub3,Sub1]
print(Sub1.subs)
#[<class __main__.Sub3 at 0x0282B2D0>, <class __main__.Sub1 at 0x01C79810>]

【讨论】:

  • OP 特别提到了他/她为什么要寻找另一种方法。
  • 在没有充分理由的情况下不必要地使事情复杂化,这是我能想到的最反蟒蛇行为:-)
【解决方案7】:

没有办法在 Python 中直接声明前向引用,但有几种变通方法,其中一些是合理的:

1)在定义子类后手动添加。

    - Pros: easy to do; Base.subs is updated in one place
    - Cons: easy to forget (plus you don't want to do it this way)

例子:

class Base(object):
    pass

class Sub1(Base):
    pass

class Sub2(Base):
    pass

class Sub3(Base):
    pass

Base.subs = [sub3, sub1]

2) 使用 str 值创建 Base.subs,并使用 class decorator 替换实际的子类(这可以是 Base 上的类方法或函数 - 我是显示函数版本,虽然我可能会使用方法版本)。

- Pros: easy to do
- Cons: somewhat easy to forget; 

例子:

def register_with_Base(cls):
    name = cls.__name__
    index = Base.subs.index(name)
    Base.subs[index] = cls
    return cls

class Base(object):
    subs = ['Sub3', 'Sub1']

@register_with_Base
class Sub1(Base):
    pass

class Sub2(Base):
    pass

@register_with_Base
class Sub3(Base):
    pass

3) 使用str 值创建Base.subs,并让使用Base.subs 的方法进行替换。

- Pros: no extra work in decorators, no forgetting to update `subs` later
- Cons: small amount of extra work when accessing `subs`

例子:

class Base(object):
    subs = ['Sub3', 'Sub1']
    def select_sub(self, criteria):
        for sub in self.subs:
            sub = globals()[sub]
            if #sub matches criteria#:
                break
        else:
            # use a default, raise an exception, whatever
        # use sub, which is the class defined below

class Sub1(Base):
    pass

class Sub2(Base):
    pass

class Sub3(Base):
    pass

我自己会使用选项 3,因为它将功能和数据保存在一个地方。您唯一需要做的就是使subs 保持最新(当然还要编写适当的子类)。

【讨论】:

    【解决方案8】:

    有一个解决方法:将引用的虚拟 Sub1、Sub3 类放在顶部,这相当于“前向声明”。在执行时,它们将被具有相同名称的实际实现替换。

    前向声明.py:

    class Sub1(): 
        print("Sub1 dummy class called")
        pass
    class Sub3():
        print("Sub3 dummy class called")
        pass
    
    class Base:
      subs = [Sub3, Sub1]
      print("Base class called")
    
    class Sub1(Base): 
        print("Sub1 class called")
        def __init__(self):
            print("Sub1:__init__ called")
        pass
    class Sub2(Base):
        def __init__(self):
            print("Sub2:__init__ called")
        pass
    class Sub3(Base):
        def __init__(self):
            print("Sub3:__init__ called")
        pass
    
    sub_1 = Sub1()
    sub_2 = Sub2()
    sub_3 = Sub3()
    
    print(Base.subs)
    

    python 前向声明.py

    Sub1 dummy class called
    Sub3 dummy class called
    Base class called
    Sub1 class called
    Sub1:__init__ called
    Sub2:__init__ called
    Sub3:__init__ called
    [<class '__main__.Sub3'>, <class '__main__.Sub1'>]
    

    注意:上述方法在 mypy 或 pylint 静态检查中失败,但可以正常工作

    【讨论】:

      【解决方案9】:

      不是直接回答您关于前向声明的问题,而是直接解决您了解基类本身中所有子类的问题:只需使用(可能不太为人所知)__subclasses__ 函数,它应该在 Python3 中的所有类上定义。它直接为您提供您想要的:

      In [1]: class Base: pass
      
      In [2]: class Sub1(Base): pass
      
      In [3]: class Sub2(Base): pass
      
      In [4]: class Sub3(Base): pass
          
      In [5]: Base.__subclasses__()
      Out[5]: [__main__.Sub1, __main__.Sub2, __main__.Sub3]
      

      文档没有说明是否保持定义子类的顺序,但似乎确实如此。

      另请参阅this answer 了解有关您的问题的问题。

      【讨论】:

        【解决方案10】:

        如果外部类中的本地内部类可以满足给定的需求,那么一个简单的解决方案会有所帮助(因此内部类被声明为“向前”;至少在它们在外部类中被实例化之前):

        class Base:
            class Sub1:
                # ...
            class Sub2:
                # ...
            class Sub3:
                # ...
        
            __Object1 = Sub1()
            __Object2 = Sub2()
            __Object3 = Sub3()
            # ...
        

        【讨论】:

        • 在其他类中声明类被认为是不好的做法。
        猜你喜欢
        • 1970-01-01
        • 2021-12-25
        • 1970-01-01
        • 1970-01-01
        • 2023-03-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多