zoukankan      html  css  js  c++  java
  • Python 3 初探,第 2 部分: 高级主题

    Python 3 是 Guido van Rossum 功能强大的通用编程语言的最新版本。它虽然打破了与 2.x 版本的向后兼容性,但却清理了某些语法方面的问题。本文是这个由两部分组成的系列文章中的第二篇,本文构建在此系列 前一期文章 的基础之上,内容涵盖了 Python 更多的新特性和更高深的一些主题,比如在抽象基类、元类和修饰符等方面的变化。

    Cesar Otero, 顾问, 自由职业

    2009 年 5 月 04 日

    • +内容
     

    有关 Python 版本 3—,也即 Python 3000Py3K— 的前一篇文章讨论了 Python 内打破向后兼容性的一些基本变化,比如新的 print() 函数、 bytes 数据类型以及 string 类型的变化。本文是该系列文章的第 2 部分,探究了更为高深的一些主题,比如抽象基类(ABC)、元类、函数注释和修饰符(decorator)、整型数(integer literal)支持、数值类型层次结构以及抛出和捕获异常,其中的大多数特性仍然会打破与版本 2x 产品线的向后兼容性。

    类修饰符

    在 Python 之前的版本中,对方法的转换必须在方法定义之后进行。对于较长的方法,此要求将定义的重要组成部分与 Python Enhancement Proposal (PEP) 318(有关链接,请参见 参考资料)给出的外部接口定义分离。下面的代码片段演示了这一转换要求:

    清单 1. Python 3 之前版本中的方法转化
    def myMethod(self):
        # do something
    
    myMethod = transformMethod(myMethod)

    为了让此类情景更易于读懂,也为了避免必须多次重用相同的方法名,在 Python 版本 2.4 中引入了方法修饰符。

    修饰符 是一些方法,这些方法可以修改其他方法并返回一个方法或另外一个可调用对象。对它们的注释是在修饰符的名称前冠以 “at” 符号(@)— 类似 Java™ 注释的语法。清单 2 显示了实际应用中的修饰符。

    清单 2. 一个修饰符方法
    @transformMethod
    def myMethod(self):
        # do something

    修饰符是一些纯粹的语法糖(syntactic sugar)— 或者(如 Wikipedia 所言)“对计算机语言语法的补充,这些补充并不影响语言的功能,而是会让语言变得更易于被人使用。”修饰符的一种常见用法是注释静态方法。比如,清单 1 和清单 2 相当,但清单 2 更容易被人读懂。

    定义修饰符与定义其他方法无异:

    def mod(method):
        method.__name__ = "John"
        return method
    
    @mod
    def modMe():
        pass
    
    print(modMe.__name__)

    更棒的是 Python 3 现在不仅支持针对方法的修饰符,并且支持针对类的修饰符,所以,取代如下的用法:

    class myClass:
        pass
    
    myClass = doSomethingOrNotWithClass(myClass)

    我们可以这样使用:

    @doSomethingOrNotWithClass
    class myClass:
        pass

    元类

    元类 是这样一些类,这些类的实例也是类。Python 3 保留了内置的、用来创建其他元类或在运行时动态创建类的 metaclass 类型。如下的语法仍旧有效:

    >>>aClass = type('className', 
       (object,), 
       {'magicMethod': lambda cls : print("blah blah")})

    上述语法接受的参数包括:作为类名的字符串、被继承对象的元组(可以是一个空的元组)和一个包含可以添加的方法的字典(也可以是空的)。当然,也可以从类型继承并创建您自己的元类:

    class meta(type):
        def __new__(cls, className, baseClasses, dictOfMethods):
            return type.__new__(cls, className, baseClasses, dictOfMethods)

    只允许关键字的参数

    Python 3 已经更改了函数参数分配给 “参数槽(parameter slot)” 的方式,可以在传递进的参数内使用星号(*)以便不接受可变长度的参数。命名的参数必须在 * 之后 — 比如,def meth(*, arg1): pass。更多信息,请参考 Python 文档或 PEP 3102(有关链接,请参见 参考资料)。

    注意:如果上面两个例子起不到任何作用,我强烈建议您阅读 David Mertz 和 Michele Simionato 合写的有关元类的系列文章。相关链接,请参见 参考资料

    请注意,现在,在类定义中,关键字参数被允许出现在基类列表之后 — 通常来讲,即 class Foo(*bases, **kwds): pass。使用关键字参数 metaclass 将元类传递给类定义。比如:

    >>>class aClass(baseClass1, baseClass2, metaclass = aMetaClass): pass

    旧的元类语法是将此元类分配给内置属性 __metaclass__

    class Test(object):
        __metaclass__ = type

    而且,既然有了新的属性 —__prepare__— 我们就可以使用此属性为新的类名称空间创建字典。在类主体被处理之前,先会调用它,如清单 3 所示。

    清单 3. 使用 the __prepare__ attribute 的一个简单元类
    def meth():
        print("Calling method")
    
    class MyMeta(type):
        @classmethod
        def __prepare__(cls, name, baseClasses):
            return {'meth':meth}
    
        def __new__(cls, name, baseClasses, classdict):
            return type.__new__(cls, name, baseClasses, classdict)
    
    class Test(metaclass = MyMeta):
        def __init__(self):
            pass
    
        attr = 'an attribute'
    
    t = Test()
    print(t.attr)

    我们从 PEP 3115 节选了一个更为有趣的例子,如清单 4 所示,这个例子创建了一个具有其方法名称列表的元类,而同时又保持了类方法声明的顺序。

    清单 4. 保持了类成员顺序的一个元类
    # 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

    在元类内所做的这些改变有几个原因。对象的方法一般存储于一个字典,而这个字 典是没有顺序的。不过,在某些情况下,若能保持所声明的类成员的顺序将非常有用。这可以通过让此元类在信息仍旧可用时,较早地涉入类的创建得以实现 — 这很有用,比如在 C 结构的创建中。借助这种新的机制还能在将来实现其他一些有趣的功能,比如在类构建期间将符号插入到所创建的类名称空间的主体以及对符号的前向引用。PEP 3115 提到更改语法还有美学方面的原因,但是对此尚存在无法用客观标准解决的争论(到 PEP 3115 的链接,请参见 参考资料)。

    抽象基类

    正如我在 Python 3 初探,第 1 部分:Python 3 的新特性 中提到的,ABC 是一些不能被实例化的类。Java 或 C++ 语言的程序员应该对此概念十分熟悉。Python 3 添加了一个新的框架 —abc— 它提供了对 ABC 的支持。

    这个 abc 模块具有一个元类(ABCMeta)和 修饰符(@abstractmethod@abstractproperty)。如果一个 ABC 具有一个 @abstractmethod@abstractproperty,它就不能被实例化,但必须在一个子类内被覆盖。比如,如下代码:

    >>>from abc import *
    >>>class C(metaclass = ABCMeta): pass
    >>>c = C()

    这些代码是可以的,但是不能像下面这样编码:

    >>>from abc import *
    >>>class C(metaclass = ABCMeta): 
    ...    @abstractmethod
    ...    def absMethod(self):
    ...        pass
    >>>c = C() 
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: Can't instantiate abstract class C with abstract methods absMethod

    更好的做法是使用如下代码:

    >>>class B(C):
    ...    def absMethod(self):
    ...        print("Now a concrete method")
    >>>b = B()
    >>>b.absMethod()
    Now a concrete method

    ABCMeta 类覆盖属性 __instancecheck____subclasscheck__,借此可以重载内置函数 isinstance()issubclass()。要向 ABC 添加一个虚拟子类,可以使用 ABCMeta 提供的 register() 方法。如下所示的简单示例:

    >>>class TestABC(metaclass=ABCMeta): pass
    >>>TestABC.register(list)
    >>>TestABC.__instancecheck__([])
    True

    它等同于使用 isinstance(list, TestABC)。您可能已经注意到 Python 3 使用 __instancecheck__,而非 __issubclass__,使用 __subclasscheck__,而非 __issubclass__,这看起来更为自然。若将参数 isinstance(subclass, superclass) 反转成,比如 superclass.__isinstance__(subclass),可能会引起混淆。可见,语法 superclass.__instancecheck__(subclass) 显然更好一点。

    collections 模块内,可以使用几个 ABC 来测试一个类是否提供了特定的一个接口:

    >>>from collections import Iterable
    >>>issubclass(list, Iterable)
    True

    表 1 给出了这个集合框架的 ABC。

    表 1. 这个集合框架的 ABC
    ABCInherits
    Container  
    Hashable  
    Iterable  
    Iterator Iterable
    Sized  
    Callable  
    Sequence Sized, Iterable, Container
    MutableSequence Sequence
    Set Sized, Iterable, Container
    MutableSet Set
    Mapping Sized, Iterable, Container
    MutableMapping Mapping
    MappingView Sized
    KeysView MappingView, Set
    ItemsView MappingView, Set
    ValuesView MappingView

    集合

    集合框架包括容器数据类型、双端队列(即 deque)以及一个默认的字典(即 defaultdict)。一个 deque 支持从前面或后面进行追加和弹出。defaultdict 容器是内置字典的一个子类,它(根据 Python 3 文档)“覆盖一个方法并添加一个可写的实例变量。”除此之外,它还充当一个字典。此外,集合框架还提供了一个数据类型工厂函数 namedtuple()

    ABC 类型层次结构

    Python 3 现支持能代表数值类的 ABC 的类型层次结构。这些 ABC 存在于 numbers 模块内并包括 NumberComplexRealRationalIntegral。图 1 显示了这个数值层次结构。可以使用它们来实现您自己的数值类型或其他数值 ABC。

    图 1. 数值层次结构

    数值层次结构

    数值塔(numerical tower)

    Python 的数值层次结构的灵感来自于 Scheme 语言的数值塔。

    新模块 fractions 可实现这个数值 ABC Rational。此模块提供对有理数算法的支持。若使用 dir(fractions.Fraction),就会注意到它具有一些属性,比如 imagreal__complex__。根据数值塔的原理分析,其原因在于 Rationals 继承自 Reals,而 Reals 继承自 Complex

    抛出和捕获异常

    在 Python 3 内,except 语句已经被修改为处理语法不清的问题。之前,在 Python version 2.5 内,try . . . except 结构,比如:

    >>>try:
    ...    x = float('not a number')
    ... except (ValueError, NameError):
    ...    print "can't convert type"

    可能会被不正确地写为:

    >>> try:
    ...    x = float('not a number')
    ... except ValueError, NameError:
    ...    print "can't convert type"

    后一种格式的问题在于 ValueError 异常将永远捕获不到,因为解释器将会捕获 ValueError 并将此异常对象绑定到名称 NameError。这一点可以从下面的示例中看出:

    >>> try:
    ...    x = float('blah')
    ... except ValueError, NameError:
    ...    print "NameError is ", NameError
    ...
    NameError is invalid literal for float(): not a number

    所以,为了处理语法不清的问题,在想要将此异常对象与另一个名称绑定时,逗号(,)会被替换成关键字 as。如果想要捕获多个异常,必须使用括号(())。清单 5 中的代码展示了 Python 3 内的两个合乎语法的示例。

    清单 5. Python 3 内的异常处理
    # bind ValueError object to local name ex
    try:
        x = float('blah')
    except ValueError as ex:
        print("value exception occurred ", ex)
     
    # catch two different exceptions simultaneously
    try:
        x = float('blah')
    except (ValueError, NameError):
        print("caught both types of exceptions")

    异常处理的另一个改变是异常链— 隐式或显式。清单 6 给出了隐式异常链的一个示例。

    清单 6. Python 3 内的隐式异常链
    def divide(a, b):
        try:
            print(a/b)
        except Exception as exc:
            def log(exc):
            fid = open('logfile.txt') # missing 'w'
            print(exc, file=fid)
            fid.close()
    
            log(exc)
    
    divide(1,0)

    divide() 方法试图执行除数为零的除法,因而引发了一个异常:ZeroDivisionError。但是,在异常语句的嵌套 log() 方法内,print(exc, file=fid) 试图向一个尚未打开的文件进行写操作。Python 3 抛出异常,如清单 7 所示。

    清单 7. 隐式异常链示例的跟踪
    Traceback (most recent call last):
      File "chainExceptionExample1.py", line 3, in divide
      print(a/b)
    ZeroDivisionError: int division or modulo by zero
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "chainExceptionExample1.py", line 12, in <module>
        divide(1,0)
      File "chainExceptionExample1.py", line 10, in divide
        log(exc)
      File "chainExceptionExample1.py", line 7, in log
        print(exc, file=fid)
      File "/opt/python3.0/lib/python3.0/io.py", line 1492, in write
        self.buffer.write(b)
      File "/opt/python3.0/lib/python3.0/io.py", line 696, in write
        self._unsupported("write")
      File "/opt/python3.0/lib/python3.0/io.py", line 322, in _unsupported
        (self.__class__.__name__, name))
    io.UnsupportedOperation: BufferedReader.write() not supported

    请注意,这两个异常都被处理。在 Python 的早期版本中,ZeroDivisionError 将会丢失,得不到处理。那么这是如何实现的呢?__context__ 属性,比如 ZeroDivisionError,现在是所有异常对象的一部分。在本例中,被抛出的 IOError__context__ 属性 “仍为” __context__ 属性内的 ZeroDivisionError

    __context__ 属性外,异常对象还有一个 __cause__ 属性,它通常被初始化为 None。这个属性的作用是以一种显式方法记录异常的原因。__cause__ 属性通过如下语法设置:

    >>> raise EXCEPTION from CAUSE

    它与下列代码相同:

    >>>exception = EXCEPTION
    >>>exception.__cause__ = CAUSE
    >>>raise exception

    但更为优雅。清单 8 中所示的示例展示了显式异常链。

    清单 8. Python 3 中的显式异常链
    class CustomError(Exception): 
        pass
    
    try:
        fid = open("aFile.txt") # missing 'w' again
        print("blah blah blah", file=fid)
    except IOError as exc:
        raise CustomError('something went wrong') from exc

    正如之前的例子所示,print() 函数抛出了一个异常,原因是文件尚未打开以供写入。清单 9 给出了相应的跟踪。

    清单 9. 异常跟踪
    Traceback (most recent call last):
      File "chainExceptionExample2.py", line 5, in <module>
        fid = open("aFile.txt")
      File "/opt/python3.0/lib/python3.0/io.py", line 278, in __new__
        return open(*args, **kwargs)
      File "/opt/python3.0/lib/python3.0/io.py", line 222, in open
        closefd)
     File "/opt/python3.0/lib/python3.0/io.py", line 615, in __init__
        _fileio._FileIO.__init__(self, name, mode, closefd)
    IOError: [Errno 2] No such file or directory: 'aFile.txt'
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
    File "chainExceptionExample2.py", line 8, in <modulei>
      raise CustomError('something went wrong') from exc
    __main__.CustomError: something went wrong

    请注意,在异常跟踪中的一行 “The above exception was the direct cause of the following exception,” 之后的是另一个对导致 CustomError “something went wrong” 异常的跟踪。

    添加给异常对象的另一个属性是 __traceback__。如果被捕获的异常不具备其 __traceback__ 属性,那么新的跟踪就会被设置。如下是一个简单的例子:

    from traceback import format_tb
    
    try:
        1/0
    except Exception as exc:
        print(format_tb(exc.__traceback__)[0])

    请注意,format_tb 返回的是一个列表,而且此列表中只有一个异常。

    with 语句

    从 Python 版本 2.6 开始,with 已经成为了一个关键字,而且这一属性无需再通过 __future__ 导入。

    整数支持和语法

    Python 支持不同进制的整型字符串文本 — 八进制、十进制(最明显的!)和十六进制 — 而现在还加入了二进制。八进制数的表示已经改变:八进制数现在均以一个前缀 0o0O(即,数字零后跟一个大写或小写的字母 o)开头。比如,八进制的 13 或十进制的 11 分别如下表示:

    >>>0o13
    11

    新的二进制数则以前缀 0b0B(即,数字零后跟一个大写或小写的字母 b)开头。十进制数 21 用二进制表示应为:

    >>>0b010101
    21

    oct()hex() 方法已被删除。

    函数注释

    函数注释 会在编译时将表述与函数的某些部分(比如参数)相关联。就其本身而言,函数注释是无意义的 — 即,除非第三方库对之进行处理,否则它们不会被处理。函数注释的作用是为了标准化函数参数或返回值被注释的方式。函数注释语法为:

    def methodName(param1: expression1, ..., paramN: expressionN)->ExpressionForReturnType:
        ...

    例如,如下所示的是针对某函数的参数的注释:

    def execute(program:"name of program to be executed", error:"if something goes wrong"):
        ...

    如下的示例则注释了某函数的返回值。这对于检查某函数的返回类型非常有用:

    def getName() -> "isString":
         ...

    函数注释的完整语法可以在 PEP 3107(相关链接,请参见 参考资料)内找到。

    结束语

    意外收获

    在我看来,为 Python 3 添加的最棒的一个模块是 antigravity。启动 Python,然后在命令行键入 import antigravity。您一定不会失望。

    Python 3 最终的发布版在 2008 年 12 月份初就已经发布。自那之后,我曾经查阅过一些博客,试图了解人们对向后不兼容性问题的反应。虽然,我不能断言社区在某些程度上已经达成了官方的共识,但 我阅读过的博客所呈现的观点呈两极分化。Linux® 开发社区似乎不太喜欢转变到版本 3,因为需要移植大量代码。相比之下,因为新版本对 unicode 支持方面的改进,很多 Web 开发人员则欢迎转变。

    我想要说明的一点是,在您做决定是否要移植到新版本之前,应该仔细阅 读相关的 PEP 和开发邮件列表。这些 PEP 解释了各项更改的初衷、带来的益处及其实现。不难看出,这些更改的做出经过了深思熟虑和激烈讨论。本系列所展示的这些主题旨在让普通的 Python 程序员无需遍阅所有的 PEP 就能立即对这些更改有大致的概念。

  • 相关阅读:
    如何手动封装 $ on off emit?
    Vue 实例身上的一些方法(二)
    Vue 实例身上的一些方法(一)
    Vue属性过滤
    Vue属性监听
    Vue实现简单的商品增减功能
    Vue 计算属性
    使用Vue实现一个简单地自定义拖拽功能
    数组的深拷贝与浅拷贝
    如何让html引用公共布局(多个html文件公用一个header.html和footer.html)
  • 原文地址:https://www.cnblogs.com/Simon-xm/p/4069281.html
Copyright © 2011-2022 走看看