Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 359 – “make” 语句

作者:
Steven Bethard <steven.bethard at gmail.com>
状态:
已撤回
类型:
标准轨迹
创建:
2006年4月5日
Python 版本:
2.6
历史记录:
2006年4月5日、2006年4月6日、2006年4月13日

目录

摘要

本 PEP 提出了一种类声明语法的泛化,即 make 语句。建议的语法和语义与类定义的语法相似,因此

make <callable> <name> <tuple>:
    <block>

被转换为赋值

<name> = <callable>("<name>", <tuple>, <namespace>)

其中 <namespace> 是通过执行 <block> 创建的字典。这主要是在语法上等同于

class <name> <tuple>:
    __metaclass__ = <callable>
    <block>

并且旨在帮助更清晰地表达语句的意图,当创建的对象不是类时。当然,此类语句的其他语法也是可能的,但希望通过与类语句保持强烈的相似性,对类和元类工作原理的理解也能转化为对 “make” 语句工作原理的理解。

本 PEP 基于 Michele Simionato 在 python-dev 列表上的一个建议 [1]

撤回通知

本 PEP 已应 Guido 的请求撤回 [2]。Guido 不喜欢它,尤其是它不喜欢属性用例如何将属性的实例方法置于与其他实例方法不同的级别,并且需要为属性函数指定固定的名称。

动机

类语句为 Python 提供了两个不错的功能

  1. 它们执行一段语句,并将结果绑定作为字典提供给元类。
  2. 它们通过允许正在创建的类知道其被分配的名称来鼓励 DRY(不要重复自己)。

因此,在像这样的简单类语句中

class C(object):
    x = 1
    def foo(self):
        return 'bar'

元类(type)会以类似以下内容的方式被调用

C = type('C', (object,), {'x':1, 'foo':<function foo at ...>})

类语句只是上述赋值语句的语法糖,但显然是一种非常有用的语法糖。它不仅避免了 C 的重复,还通过允许将其表示为一系列语句来简化字典的创建。

从历史上看,类型实例(也称为类对象)是唯一拥有此类语法支持的对象。“make” 语句旨在将这种支持扩展到其他类型的对象,在这些对象上,这种语法也很有用。

示例:简单的命名空间

假设我在一个模块中有一些属性,我像这样访问它们

mod.thematic_roletype
mod.opinion_roletype

mod.text_format
mod.html_format

并且由于“命名空间是一个很棒的主意”,我想能够像这样访问这些属性

mod.roletypes.thematic
mod.roletypes.opinion

mod.format.text
mod.format.html

我目前有两个主要选项

  1. 将模块转换为包,将 roletypesformat 转换为子模块,并将属性移动到子模块中。
  2. 创建 roletypesformat 类,并将属性移动到类中。

前者是一大块重构工作,并且生成了两个内容很少的小模块。后者使属性在模块中保持本地化,但在没有创建这些类的实例的意图时创建了类。

在这种情况下,能够简单地声明一个“命名空间”来保存少量属性会很好。使用新的 “make” 语句,我可以使用以下内容引入新的命名空间

make namespace roletypes:
    thematic = ...
    opinion = ...

make namespace format:
    text = ...
    html = ...

并将我的属性保持在模块本地,而无需创建从不打算实例化的类。一个可以使此工作生效的命名空间定义是

class namespace(object):
    def __init__(self, name, args, kwargs):
        self.__dict__.update(kwargs)

给定此定义,在上面的 “make” 语句结束时,roletypesformat 将是命名空间实例。

示例:GUI 对象

在 GUI 工具包中,框架和面板等对象通常与属性和函数相关联。使用 “make” 语句,类似以下内容的代码

root = Tkinter.Tk()
frame = Tkinter.Frame(root)
frame.pack()
def say_hi():
    print "hi there, everyone!"
hi_there = Tkinter.Button(frame, text="Hello", command=say_hi)
hi_there.pack(side=Tkinter.LEFT)
root.mainloop()

可以改写为将 Button 的函数与其声明分组

root = Tkinter.Tk()
frame = Tkinter.Frame(root)
frame.pack()
make Tkinter.Button hi_there(frame):
    text = "Hello"
    def command():
        print "hi there, everyone!"
hi_there.pack(side=Tkinter.LEFT)
root.mainloop()

示例:自定义描述符

由于描述符用于自定义对属性的访问,因此了解该属性的名称通常很有用。当前的 Python 没有提供一种简单的方法来查找此名称,因此许多自定义描述符,例如 Ian Bicking 的 setonce 描述符 [3],必须以某种方式解决此问题。使用 “make” 语句,您可以创建类似以下内容的 setonce 属性

class A(object):
    ...
    make setonce x:
        "A's x attribute"
    ...

其中 setonce 描述符将定义为

class setonce(object):

    def __init__(self, name, args, kwargs):
        self._name = '_setonce_attr_%s' % name
        self.__doc__ = kwargs.pop('__doc__', None)

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return getattr(obj, self._name)

    def __set__(self, obj, value):
        try:
            getattr(obj, self._name)
        except AttributeError:
            setattr(obj, self._name, value)
        else:
            raise AttributeError("Attribute already set")

    def set(self, obj, value):
        setattr(obj, self._name, value)

    def __delete__(self, obj):
        delattr(obj, self._name)

请注意,与原始实现不同,私有属性名称是稳定的,因为它使用描述符的名称,因此类 A 的实例是可腌制的。

示例:属性命名空间

Python 的 property 类型采用三个函数参数和一个文档字符串参数,尽管这些参数仅与属性相关,但必须在它之前声明,然后作为参数传递给 property 调用,例如

class C(object):
    ...
    def get_x(self):
        ...
    def set_x(self):
        ...
    x = property(get_x, set_x, "the x of the frobulation")

此问题之前曾被提出过,Guido [4] 和其他人 [5] 曾经简要地思考过其他属性语法以简化属性的声明。使用 “make” 语句,可以支持以下语法

class C(object):
    ...
    make block_property x:
        '''The x of the frobulation'''
        def fget(self):
            ...
        def fset(self):
            ...

以及以下 block_property 的定义

def block_property(name, args, block_dict):
    fget = block_dict.pop('fget', None)
    fset = block_dict.pop('fset', None)
    fdel = block_dict.pop('fdel', None)
    doc = block_dict.pop('__doc__', None)
    assert not block_dict
    return property(fget, fset, fdel, doc)

示例:接口

Guido [6] 和其他人偶尔会建议将接口引入 python。大多数建议都提供了类似以下内容的语法

interface IFoo:
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

但由于目前在 Python 中没有办法以这种方式声明接口,因此大多数 Python 接口的实现都使用类对象,例如 Zope 的

class IFoo(Interface):
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

使用新的 “make” 语句,这些接口可以改为声明为

make Interface IFoo:
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

这使得意图(这是一个接口,而不是一个类)更加清晰。

规范

Python 将转换 “make” 语句

make <callable> <name> <tuple>:
    <block>

为赋值

<name> = <callable>("<name>", <tuple>, <namespace>)

其中 <namespace> 是通过执行 <block> 创建的字典。<tuple> 表达式是可选的;如果不存在,则假定为空元组。

有一个补丁实现了这些语义 [7]

“make” 语句引入了新的关键字 make。因此,在 Python 2.6 中,必须使用 from __future__ import make_statement 启用 “make” 语句。

未解决的问题

关键词

关键字 make 是否破坏了太多代码?最初,“make” 语句使用关键字 create(Alyssa Coghlan 的建议)。但是,对标准库 [8] 和 Zope+Plone 代码 [9] 的调查表明,create 会破坏更多代码,因此 make 被采用为关键字。但是,仍然有一些情况 make 会破坏代码。是否有更好的语句关键字?

标准库(以及一些已安装的包)中的一些可能关键字及其计数

  • make - 2(都在测试中)
  • create - 19(包括 imaplib 中现有的函数)
  • build - 83(包括 distutils.command.build 中现有的类)
  • construct - 0
  • produce - 0

“make” 语句作为替代构造函数

目前,没有多少函数具有 (name, args, kwargs) 签名。这意味着像这样的内容

make dict params:
    x = 1
    y = 2

目前是不可能的,因为字典构造函数具有不同的签名。是否需要支持这种事情?Carl Banks 的一个建议是添加一个 __make__ 魔术方法,如果找到该方法,则调用它而不是 __call__。对于类型,__make__ 方法将与 __call__ 相同,因此没有必要,但字典可以通过在字典类型上定义一个类似于以下内容的 __make__ 方法来支持 “make” 语句

def __make__(cls, name, args, kwargs):
    return cls(**kwargs)

当然,与其添加另一个魔术方法,不如让字典类型增加一个类似于 dict.fromblock 的类方法,它可以像这样使用

make dict.fromblock params:
    x = 1
    y = 2

所以问题是,许多类型是否希望将 “make” 语句用作替代构造函数?如果是这样,这个替代构造函数是否需要与原始构造函数具有相同的名称?

自定义执行代码块的字典

“make” 语句的用户是否应该能够确定在哪个字典对象中执行代码?这将允许 “make” 语句用于普通字典对象不适用的情况,例如,如果必须允许顺序和重复名称。允许这种自定义可以允许编写 XML 而无需重复元素名称,并且使 “make” 语句的嵌套对应于 XML 元素的嵌套

make Element html:
    make Element body:
        text('before first h1')
        make Element h1:
            attrib(style='first')
            text('first h1')
            tail('after first h1')
        make Element h1:
            attrib(style='second')
            text('second h1')
            tail('after second h1')

如果 “make” 语句试图通过调用可调用对象的 __make_dict__ 方法来获取要在其上执行代码块的字典,则以下代码将允许 “make” 语句按上述方式使用

class Element(object):

    class __make_dict__(dict):

        def __init__(self, *args, **kwargs):
            self._super = super(Element.__make_dict__, self)
            self._super.__init__(*args, **kwargs)
            self.elements = []
            self.text = None
            self.tail = None
            self.attrib = {}

        def __getitem__(self, name):
            try:
                return self._super.__getitem__(name)
            except KeyError:
                if name in ['attrib', 'text', 'tail']:
                    return getattr(self, 'set_%s' % name)
                else:
                    return globals()[name]

        def __setitem__(self, name, value):
            self._super.__setitem__(name, value)
            self.elements.append(value)

        def set_attrib(self, **kwargs):
            self.attrib = kwargs

        def set_text(self, text):
            self.text = text

        def set_tail(self, text):
            self.tail = text

    def __new__(cls, name, args, edict):
        get_element = etree.ElementTree.Element
        result = get_element(name, attrib=edict.attrib)
        result.text = edict.text
        result.tail = edict.tail
        for element in edict.elements:
            result.append(element)
        return result

但是,请注意,支持此功能的代码有些脆弱——它必须以某种“魔法”方式用attribtexttail填充命名空间,并且它假设make语句体内的每个名称绑定都在创建一个Element。按照目前的状况,如果在任何一个make语句体中引入简单的for循环,此代码就会失效,因为for循环会将一个名称绑定到一个非Element对象。可以通过添加某种isinstance检查或属性检查来解决此问题,但这仍然会导致解决方案比较脆弱。

还有人指出,with语句可以用更明确的语法提供等效的嵌套。

with Element('html') as html:
    with Element('body') as body:
        body.text = 'before first h1'
        with Element('h1', style='first') as h1:
            h1.text = 'first h1'
            h1.tail = 'after first h1'
        with Element('h1', style='second') as h1:
            h1.text = 'second h1'
            h1.tail = 'after second h1'

如果这里元素名称的重复过于违反DRY原则,也可以通过向Element添加一些方法来消除除第一个之外的所有as子句。[10]

那么,在不同类型的字典中执行块是否存在实际用例?如果是,是否应该扩展make语句以支持它们?

可选扩展

移除 “make” 关键字

可能可以删除make关键字,以便此类语句以可调用对象被调用开头,例如:

namespace ns:
    badger = 42
    def spam():
        ...

interface C(...):
    ...

但是,几乎所有其他Python语句都以关键字开头,删除关键字会使在文档中查找此结构变得更加困难。此外,这会在语法中增加一些复杂性,到目前为止,我(Steven Bethard)还没有能够在没有关键字的情况下实现此功能。

在 Python 3000 中移除 __metaclass__

作为其通用性的副作用,make语句主要消除了类对象中__metaclass__属性的必要性。因此,在Python 3000中,而不是

class <name> <bases-tuple>:
    __metaclass__ = <metaclass>
    <block>

元类可以通过使用元类作为make语句中的可调用对象来支持。

make <metaclass> <name> <bases-tuple>:
    <block>

删除__metaclass__钩子将稍微简化BUILD_CLASS操作码。

在 Python 3000 中移除类语句

在make语句的最极端应用中,类语句本身可以被弃用,转而使用make type语句。

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0359.rst

上次修改:2023-10-11 12:05:51 GMT