繁体   English   中英

了解 __init_subclass__

[英]Understanding __init_subclass__

我终于升级了我的 python 版本,我发现了添加的新功能。 除其他外,我对新的__init_subclass__方法摸不着头脑。 从文档:

每当包含 class 的子类化时,都会调用此方法。 cls 是新的子类。 如果定义为普通实例方法,则此方法隐式转换为 class 方法。

所以我开始玩弄它,按照文档中的示例:

class Philosopher:
    def __init_subclass__(cls, default_name, **kwargs):
        super().__init_subclass__(**kwargs)
        print(f"Called __init_subclass({cls}, {default_name})")
        cls.default_name = default_name

class AustralianPhilosopher(Philosopher, default_name="Bruce"):
    pass

class GermanPhilosopher(Philosopher, default_name="Nietzsche"):
    default_name = "Hegel"
    print("Set name to Hegel")

Bruce = AustralianPhilosopher()
Mistery = GermanPhilosopher()
print(Bruce.default_name)
print(Mistery.default_name)

产生这个 output:

Called __init_subclass(<class '__main__.AustralianPhilosopher'>, 'Bruce')
'Set name to Hegel'
Called __init_subclass(<class '__main__.GermanPhilosopher'>, 'Nietzsche')
'Bruce'
'Nietzsche'

我知道这个方法是在子类定义之后调用的,但我的问题特别是关于这个特性的使用。 我也阅读了PEP 487文章,但对我帮助不大。 这种方法在哪里有帮助? 是否为:

  • 在创建时注册子类的超类?
  • 强制子类在定义时设置字段?

另外,我是否需要了解__set_name__才能完全理解它的用法?

PEP 487开始采用两个常见的元类用例,使它们更易于访问,而不必了解元类的所有来龙去脉。 否则, __init_subclass____set_name__这两个新功能是相互独立的 ,它们__set_name__不依赖。

__init_subclass__只是一个挂钩方法。 您可以将其用于任何您想要的东西。 这对于以某种方式注册子类以及在这些子类上设置默认属性值很有用。

我们最近使用它为不同的版本控制系统提供“适配器”,例如:

class RepositoryType(Enum):
    HG = auto()
    GIT = auto()
    SVN = auto()
    PERFORCE = auto()

class Repository():
    _registry = {t: {} for t in RepositoryType}

    def __init_subclass__(cls, scm_type=None, name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if scm_type is not None:
            cls._registry[scm_type][name] = cls

class MainHgRepository(Repository, scm_type=RepositoryType.HG, name='main'):
    pass

class GenericGitRepository(Repository, scm_type=RepositoryType.GIT):
    pass

这使我们可以轻松地为特定的存储库定义处理程序类,而不必求助于使用元类或装饰器。

__init_subclass____set_name__是正交机制-它们并不相互绑定,只是在同一PEP中进行了描述。 两者都是以前需要功能齐全的元类的功能。 该PEP 487地址元类最常见的用途的2:

  • 如何让父级知道何时将其子类化( __init_subclass__
  • 如何让描述符类知道它用于的属性的名称( __set_name__

正如PEP所说:

尽管有很多使用元类的方法,但绝大多数用例可分为三类:在类创建后运行的一些初始化代码,描述符的初始化以及保持类属性定义的顺序。

通过对类的创建进行简单的挂钩就可以轻松实现前两个类别:

  • __init_subclass__钩子,用于初始化给定类的所有子类。
  • 创建类时,将在该类中定义的所有属性(描述符)上调用__set_name__挂钩,并且

第三类是另一个PEP 520的主题。

还要注意,虽然__init_subclass__替代了在此类的继承树中使用元类, __set_name__ 描述符类中的__set_name__替代了对具有描述符实例的类使用metaclass。

就像PEP的标题所建议的那样, __init_subclass__是为类提供一种更简单的自定义形式。

它是一个钩子,使您可以修改类而无需了解元类,跟踪类构造的各个方面或担心元类冲突。 作为尼克·科格兰(Nick Coghlan)在本PEP早期阶段的一则消息 ,他指出:

主要预期的可读性/可维护性好处是从更清楚地区分“定制子类初始化”案例和“定制子类的运行时行为”案例的角度出发。

完整的自定义元类不提供影响范围的任何指示,而__init_subclass__更清楚地表明, __init_subclass__类创建后的行为没有持久影响。

元类被认为是魔术,这是有原因的,您不知道在创建类后它们的作用是什么。 另一方面, __init_subclass__只是另一个类方法,它运行一次然后就完成了。 (有关确切功能,请参见其文档。)


PEP 487的全部要点是简化(即消除使用的需要)元类以用于某些常见用途。

__init_subclass__负责类的后初始化,而添加了__set_name__ (仅对描述符类有意义),以简化描述符的初始化。 除此之外,它们没有关联。

还简化了提到的元类的第三种常见情况(保持定义顺序)。 通过使用命名空间的有序映射(在Python 3.6中是dict ,但这是一个实现细节:-),使用w / oa钩解决了此问题。

我想添加一些与元类和__init_subclass__相关的引用,这可能会有所帮助。

背景

引入了__init_subclass__作为创建元类的替代方法。 以下是核心开发人员之一Brett Cannon在一次演讲中PEP 487的2分钟摘要。

推荐参考

  • Guido van Rossum关于Python元类的早期历史的博客文章
  • 杰克·范德普拉斯(Jake Vanderplas)的博客文章更深入地探讨了如何实现元类

您还可以使用它在 class 上执行一次昂贵的初始化。

例如,我想用 home 的标准波浪线速记替换以我的用户开头的路径。

/Users/myuser/.profile -> ~/.profile

很简单,我可以这样写:


from pathlib import Path

class Replacer:
    def __init__(self):
        self.home = str(Path("~").expanduser())

    def replace(self, value):
        if isinstance(value,str) and value.startswith(self.home):
            value = value.replace(self.home,"~")
        return value

replacer = Replacer()
print(replacer.replace("/Users/myuser/.profile"))

但是,对于任何运行,主路径都是恒定的,无需在每次创建 Replacer 时都计算它。

使用 __init_subclass,我只能为 class 执行一次。 是的,我还可以在模块初始化时将变量分配给 class :

class Replacer:

    home = str(Path("~").expanduser())
    ...

但可能有理由希望将计算推迟到 class 第一次实际使用。 例如,在使用 Django 时,在某些情况下,当您导入models.py时,Django 可能尚未完全初始化自身。

class UselessAncestorNeededToHouseInitSubclass:
    "do-nothing"

    def __init_subclass__(cls, /, **kwargs):
        print("__init_subclass__")
        super().__init_subclass__(**kwargs)
        cls.home = str(Path("~").expanduser())

class Replacer(UselessAncestorNeededToHouseInitSubclass):
    """__init_subclass__ wont work if defined here.  It has to be on
    an ancestor
    """

    def replace(self, value):
        if isinstance(value,str) and value.startswith(self.home):
            value = value.replace(self.home,"~")
        return value

for ix in range(0,10):
    replacer = Replacer()
    print(replacer.replace("/Users/myuser/.profile"))

Output :(注意subclass_init只被调用一次):

__init_subclass__
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM