• Python 简单入门指北(二)


    Python 简单入门指北(二)

    2 函数

    2.1 函数是一等公民

    一等公民指的是 Python 的函数能够动态创建,能赋值给别的变量,能作为参传给函数,也能作为函数的返回值。总而言之,函数和普通变量并没有什么区别。

    函数是一等公民,这是函数式编程的基础,然而 Python 中基本上不会使用 lambda 表达式,因为在 lambda 表达式的中仅能使用单纯的表达式,不能赋值,不能使用 while、try 等语句,因此 lambda 表达式要么难以阅读,要么根本无法写出。这极大的限制了 lambda 表达式的使用场景。

    上文说过,函数和普通变量没什么区别,但普通变量并不是函数,因为这些变量无法调用。但如果某个类实现了 __call__ 这个魔术方法,这个类的实例就都可以像函数一样被调用:

    class Person:
        def __init__(self):
            self.name = 'bestswifter'
            self.age = 22
            self.sex = 'm'
            
        def __call__(self):
            print(self)
            
        def __str__(self):
            return 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}'.format(user=self)
                
    p = Person()
    p() # 等价于 print(p)

    2.2 函数参数

    2.2.1 函数传参

    对于熟悉 C 系列语言的人来说,函数传参的方式一目了然。默认是拷贝传值,如果传指针是引用传值。我们先来看一段简单的 Python 代码:

    
    
    def foo(arg):
        arg = 5
        print(arg)
        
    a = 1
    foo(a)
    print(a)
    # 输出 51

    这段代码的结果符合我们的预期,从这段代码来看,Python 也属于拷贝传值。但如果再看这段代码:

    
    
    def foo(arg):
        arg.append(1)
        print(arg)
        
    a = [1]
    foo(a)
    print(a) # 输出两个 [1, 1]
    你会发现参数数组在函数内部被改变了。就像是 C 语言中传递了变量的指针一样。所以 Python 到底是拷贝传值还是引用传值呢?答案都是否定的

    Python 的传值方式可以被理解为混合传值。对于那些不可变的对象(比如 1.1.2 节中介绍过的元组,还有数字、字符串类型),传值方式是拷贝传值;对于那些可变对象(比如数组和字典)则是引用传值。

    2.2.2 默认参数

    Python 的函数可以有默认值,这个功能很好用:

    
    
    def foo(a, l=[]):
        l.append(a)
        return l
    
    foo(2,[1])  # 给数组 [1] 添加一个元素 2,得到 [1,2]
    foo(2)      # 没有传入数组,使用默认的空数组,得到 [2]

    然而如果这样调用:

    
    
    foo(2)  # 利用默认参数,得到 [2]
    foo(3)  # 竟然得到了 [2, 3]

    函数调用了两次以后,默认参数被改变了,也就是说函数调用产生了副作用。这是因为默认参数的存储并不像函数里的临时变量一样存储在栈上、随着函数调用结束而释放,而是存储在函数这个对象的内部:

    
    
    foo.__defaults__  # 一开始确实是空数组
    foo(2)  # 利用默认参数,得到 [2]
    foo.__defaults__  # 如果打印出来看,已经变成 [2] 了
    foo(3)  # 再添加一个元素就得到了 [2, 3]

    因为函数 foo 作为一个对象,不会被释放,因此这个对象内部的属性也不会随着多次调用而自动重置,会一直保持上次发生的变化。基于这个前提,我们得出一个结论:函数的默认参数不允许是可变对象,比如这里的 foo 函数需要这么写:

    
    
    def foo(a, l=None):
        if l is None:
            l = []
        l.append(a)
        return l
    
    print(foo(2)) # 得到 [2]
    print(foo(3)) # 得到 [3]

    现在,给参数添加默认值的行为在函数体中完成,不会随着函数的多次调用而累积。

    对于 Python 的默认参数来说:

    如果默认值是不可变的,可以直接设置默认值,否则要设置为 None 并在函数体中设置默认值。

    2.2.3 多参数传递

    当参数个数不确定时,可以在参数名前加一个 *:
    
    def foo(*args):
        print(args)
        
    foo(1, 2, 3)  # 输出 [1, 2, 3]
    如果直接把数组作为参数传入,它其实是单个参数,如果要把数组中所有元素都作为单独的参数传入,则在数组前面加上 *:
    
    a = [1, 2, 3]   
    foo(a)  # 会输出 ([1,2,3], )   因为只传了一个数组作为参数
    foo(*a) # 输出 [1, 2, 3]
    这里的单个 * 只能接收非关键字参数,也就是仅有参数值的哪些参数。如果想接受关键字参数,需要用 ** 来表示:
    
    def foo(*args, **kwargs):
        print(args)
        print(kwargs)
        
    foo(1,2,3, a=61, b=62)
    # 第一行输出:[1, 2, 3]
    # 第二行输出:{'a': 61, 'b': 62}
    类似的,字典变量传入函数只能作为单个参数,如果要想展开并被 **kwargs 识别,需要在字典前面加上两个星号 **:
    
    a = [1, 2, 3]
    d = {'a': 61, 'b': 62}
    foo(*a, **d)

    2.2.4 参数分类

    Python 中函数的参数可以分为两大类:

    1. 定位参数(Positional):表示参数的位置是固定的。比如对于函数 foo(a, b) 来说,foo(1, 2) 和 foo(2, 1) 就是截然不同的,a 和 
      b 的位置是固定的,不可随意调换。 关键词参数(Keyword):表示参数的位置不重要,但是参数名称很重要。比如 foo(a
      = 1, b = 2) 和 foo(b = 2, a = 1) 的含义相同。

    有一种参数叫做仅限关键字(Keyword-Only)参数,比如考虑这个函数:

    
    
    def foo(*args, n=1, **kwargs):
        print(n)

    这个函数在调用时,如果参数 n 不指定名字,就会被前面的 *args 处理掉,如果指定的名字不是 n,又会被后面的 **kwargs 处理掉,所以参数 n 必须精确的以 (n = xxx) 的形式出现,也就是 Keyworld-Only。

    2.3 函数内省

    在 2.2.2 节中,我们查看了函数变量的 __defaults__ 属性,其实这就是一种内省,也就是在运行时动态的查看变量的信息。

    前文说过,函数也是对象,因此函数的变量个数,变量类型都应该有办法获取到,如果你需要开发一个框架,也许会对函数有各种奇葩的检查和校验。

    
    
    以下面这个函数为例:
    
    g = 1
    def foo(m, *args, n, **kwargs):
        a = 1
        b = 2
    首先可以获取函数名,函数所在模块的全局变量等:
    
    foo.__globals__   # 全局变量,包含了 g = 1
    foo.__name__      # foo
    我们还可以看到函数的参数,函数内部的局部变量:
    
    foo.__code__.co_varnames  # ('m', 'n', 'args', 'kwargs', 'a', 'b')
    foo.__code__.co_argcount  # 只计算参数个数,不考虑可变参数,所以得到 2
    或者用 inspect 模块来查看更详细的信息:
    
    import inspect
    sig = inspect.signature(foo)  # 获取函数签名
    
    sig.parameters['m'].kind      # POSITIONAL_OR_KEYWORD 表示可以是定位参数或关键字参数
    sig.parameters['args'].kind   # VAR_POSITIONAL 定位参数构成的数组
    sig.parameters['n'].kind      # KEYWORD_ONLY 仅限关键字参数
    sig.parameters['kwargs'].kind # VAR_KEYWORD 关键字参数构成的字典
    inspect.getfullargspec(foo)       
    # 得到:ArgSpec(args=['m', 'n'], varargs='args', keywords='kwargs', defaults=None)

    本节的新 API 比较多,但并不要求记住这些 API 的用法。再次强调,本文的写作目的是为了建立读者对 Python 的总体

    认知,了解 Python 能做什么,至于怎么做,那是文档该做的事。

    2.4 装饰器

    2.4.1 设计模式的消亡

    经典的设计模式有 23 个,虽然设计模式都是常用代码的总结,理论上来说与语法无关。但不得不承认的是,标准的设计模

    式在不同的语言中,有的因为语法的限制根本无法轻易实现(比如在 C 语言中实现组合模式),有的则因为语言的特定功能

    ,变得冗余啰嗦。

    以策略模式为例,有一个抽象的策略类,定义了策略的接口,然后使用者选择一个具体的策略类,构造他们的实例并且调

    用策略方法。具体代码可以参考:策略模式在百度百科的定义

    然而这些对象本身并没有作用,它们仅仅是可以调用相同的方法而已,只不过在 Java 中,所有的任务都需要由对象来完成。

    即使策略本身就是一个函数,但也必须把它包裹在一个策略对象中。所以在 Python 中更优雅写法是直接把策略函数作为变量使用。

    不过这就引入一个问题,如何判断某个函数是个策略呢,毕竟在面向对象的写法中,只要检查它的父类是否是抽象的策略类即可。

    也许你已经见过类似的写法:

    
    
    strategy
    def strategyA(n):
        print(n * 2)

    下面就开始介绍装饰器。

    2.4.2 装饰器的基本原理

    
    
    首先,装饰器是个函数,它的参数是被装饰的函数,返回值也是一个函数:
    
    def decorate(origin_func):  # 这个参数是被装饰的函数
        print(1)  # 先输出点东西
        return origin_func  # 把原函数直接返回
    
    @decorate    # 注意这里不是函数调用,所以不用加括号,也不用加被修饰的函数名
    def sayHello():
        print('Hello')
        
    sayHello()  # 如果没有装饰器,只会打印 'Hello',实际结果是打印 1 再打印 'Hello'
    因此,使用装饰器的这种写法:
    
    @decorate
    def foo():
        pass
    和下面这种写法是完全等价的, 初学者可以把装饰器在心中默默的转换成下一种写法,以方便理解:
    
    def foo():
        pass
    foo = decorate(foo)

    需要注意的是,装饰器函数 decorate 在模块被导入时就会执行,而被装饰的函数只在被调用时才会执行,也就是说即使不调用 sayHello 函数也会输出 1,但这样就不会输出 Hello 了。

    有了装饰器,配合前面介绍的函数对象,函数内省,我们可以做很多有意思的事,至少判断上一节中某个函数是否是策

    略是非常容易的。在装饰器中,我们还可以把策略函数都保存到数组中, 然后提供一个“推荐最佳策略”的功能, 其实就

    是遍历执行所有的策略,然后选择最好的结果。

    2.4.3 装饰器进阶

    上一节中的装饰器主要是为了介绍工作原理,它的功能非常简单,并不会改变被装饰函数的运行结果,仅仅是在导入时

    装饰函数,然后输出一些内容。换句话说,即使不执行函数,也要执行装饰器中的 print 语句,而且因为直接返回函数

    的缘故,其实没有真正的起到装饰的效果。

    如何做到装饰时不输出任何内容,仅在函数执行最初输出一些东西呢?这是常见的 AOP(面向切片编程) 的需求。这就

    要求我们不能再直接返回被装饰的函数,而是应该返回一个新的函数,所以新的装饰器需要这么写:

    
    
    def decorate(origin_func):
        def new_func():
            print(1)
            origin_func()
        return new_func
    
    @decorate
    def sayHello():
        print('Hello')
        
    sayHello() # 运行结果不变,但是仅在调用函数 sayHello 时才会输出 1

    这个例子的工作原理是,sayHello 函数作为参数 origin_func 被传到装饰器中,经过装饰以后,它实际上变成了 new_func,会先输出 1 再执行原来的函数,也就是 sayHello

    这个例子很简陋,因为我们知道了 sayHello 函数没有参数,所以才能定义一个同样没有参数的替代者:nwe_func。如果我们在开发一个框架,要求装饰器能对任意函数生效,就需要用到 2.2.3 中介绍的 *** 这种不定参数语法了。

    
    
    如果查看 sayHello 函数的名字,得到的结果将是 new_func:
    
    sayHello.__name__  # new_func
    这是很自然的,因为本质上其实执行的是:
    
    new_func = decorate(sayHello)
    而装饰器的返回结果是另一个函数 new_func,两者仅仅是运行结果类似,但两个对象并没有什么关联。
    
    所以为了处理不定参数,并且不改变被装饰函数的外观(比如函数名),我们需要做一些细微的修补工作。这些工作都是模板代码,所以 Python 早就提供了封装:
    
    import functools
    
    def decorate(origin_func):
        @functools.wraps(origin_func)  # 这是 Python 内置的装饰器
        def new_func(*args, **kwargs):
            print(1)
            origin_func(*args, **kwargs)
        return new_func

    2.4.4 装饰器工厂

    在 2.4.2 节的代码注释中我解释过,装饰器后面不要加括号,被装饰的函数自动作为参数,传递到装饰器函数中。如果加了

    括号和参数,就变成手动调用装饰器函数了,大多数时候这与预期不符(因为装饰器的参数一般都是被装饰的函数)。

    不过装饰器可以接受自定义的参数,然后返回另一个装饰器,这样外面的装饰器实际上就是一个装饰器工厂,可以根据用户

    的参数,生成不同的装饰器。还是以上面的装饰器为例,我希望输出的内容不是固定的 1,而是用户可以指定的,代码就应该这么写:

    import functools
    
    def decorate(content):                        # 这其实是一个装饰器工厂
        def real_decorator(origin_func):          # 这才是刚刚的装饰器
            @functools.wraps(origin_func)
            def new_func():
                print('You said ' + str(content)) # 现在输出内容可以由用户指定
                origin_func()
            return new_func                       # 在装饰器里,返回的是新的函数
        return real_decorator                     # 装饰器工厂返回的是装饰器
    装饰器工厂和装饰器的区别在于它可以接受参数,返回一个装饰器:
    
    @decorate(2017)
    def sayHello():
        print('Hello')
        
    sayHello()
    其实等价于:
    
    real_decorator = decorate(2017)      # 通过装饰器工厂生成装饰器
    new_func = real_decorator(sayHello)  # 正常的装饰器工作逻辑
    new_func()                           # 调用的是装饰过的函数

    3 面向对象

    3.1 对象内存管理

    3.1.1 对象不是盒子

    C 语言中我们定义变量用到的语法是:

    int a = 1;
    

    这背后的含义是定义了一个 int 类型的变量 a,相当于申请了一个名为 a 的盒子(存储空间),里面装了数字 1。

    图片名称
    图片名称

    然后我们改变 a 的值:a = 2;,可以打印 a 的地址来证明它并没有发生变化。所以只是盒子里装的内容(指针指向的位置)

    发生了改变:

    图片名称
    图片名称

    但是在 Python 中,变量不是盒子。比如同样的定义变量:

    a = 1
    

    这里就不能把 a 理解为 int 类型的变量了。因为在 Python 中,变量没有类型,值才有,或者说只有对象才有类型。

    因为即使是数字 1,也是 int 类的实例,而变量 a 更像是给这个对象贴的一个标签。

    图片名称
    图片名称

    如果执行赋值语句 a = 2,相当于把标签 a 贴在另一个对象上:

    图片名称
    图片名称

    基于这个认知,我们现在应该更容易理解 2.2.1 节中所说的函数传参规则了。如果传入的是不可变类型,比如 int,改变它的值实际上就是把标签挂在新的对象上,自然不会改变原来的参数。如果是可变类型,并且做了修改,那么函数中的变量和外面的变量都是指向同一个对象的标签,所以会共享变化。

    3.1.2 默认浅复制

    
    
    根据上一节的描述,直接把变量赋值给另一个变量, 还算不上复制:
    
    a = [1, 2, 3]
    b = a
    b == a   # True,等同性校验,会调用 __eq__ 函数,这里只判断内容是否相等
    b is a   # True,一致性校验,会检查是否是同一个对象,调用 hash() 函数,可以理解为比较指针
    可见不仅仅数组相同,就连变量也是相同的,可以把 b 理解为 a 的别名。
    
    如果用切片,或者数组的构造函数来创建新的数组,得到的是原数组的浅拷贝:
    
    a = [1, 2, 3]
    b = list(a)
    b == a   # True,因为数组内容相同
    b is a   # False,现在 a 和 b 是两个变量,恰好指向同一个数组对象
    但如果数组中的元素是可变的,可以看到这些元素并没有被完全拷贝:
    
    a = [[1], [2], [3]]
    b = list(a)
    b[0].append(2)
    a # 得到 [[1, 2], [2], [3]],因为 a[0] 和 b[0] 其实还是挂在相同对象上的不同标签
    如果想要深拷贝,需要使用 copy 模块的 deepcopy 函数:
    
    import copy 
    
    b = copy.deepcopy(a)
    b[0].append(2)
    a  # 变成了 [[1, 2], [2], [3]]
    a  # 还是 [[1], [2], [3]]

    此时,不仅仅是每个元素的引用被拷贝,就连每个元素自己也被拷贝。所以现在的 a[0]b[0] 是指向两个不同对象的两个不同变量(标签),自然就互不干扰了。

    如果要实现自定义对象的深复制,只要实现 __deepcopy__ 函数即可。这个概念在几乎所有面向对象的语言中都会存在,就不详细介绍了。

    3.1.3 弱引用

    Python 内存管理使用垃圾回收的方式,当没有指向对象的引用时,对象就会被回收。然而对象一直被持有也并非什

    么好事,比如我们要实现一个缓存,预期目标是缓存中的内容随着真正对象的存在而存在,随着真正对象的消失而

    消失。如果因为缓存的存在,导致被缓存的对象无法释放,就会导致内存泄漏。

    Python 提供了语言级别的支持,我们可以使用 weakref 模块,它提供了 weakref.WeakValueDictionary

    个弱引用字典来确保字典中的值不会被引用。如果想要获取某个对象的弱引用,可以使用 weakref.ref(obj) 函数。

    3.2 Python 风格的对象

    3.2.1 静态函数与类方法

    
    
    静态函数其实和类的方法没什么关系,它只是恰好定义在类的内部而已,所以这里我用函数(function) 来形容它。它可以没有参数:
    
    class Person:
        @staticmethod   # 用 staticmethod 这个修饰器来表明函数是静态的
        def sayHello():
            print('Hello')
        
    Person.sayHello() # 输出 'Hello`
    静态函数的调用方式是类名加上函数名。类方法的调用方式也是这样,唯一的不同是需要用 @staticmethod 修饰器,而且方法的第一个参数必须是类:
    
    class Person:
        @classmethod    # 用 classmethod 这个修饰器来表明这是一个类方法
        def sayHi(cls):
            print('Hi: ' + cls.__name__)
        
    Person.sayHi() # 输出 'Hi: Person`

    类方法和静态函数的调用方法一致,在定义时除了修饰器不一样,唯一的区别就是类方法需要多声明一个参数。

    这样看起来比较麻烦,但静态函数无法引用到类对象,自然就无法访问类的任何属性。

    于是问题来了,静态函数有何意义呢?有的人说类名可以提供命名空间的概念,但在我看来这种解释并不成立,

    因为每个 Python 文件都可以作为模块被别的模块引用,把静态函数从类里抽取出来,定义成全局函数,也是有命名空间的:

    
    
    # 在 module1.py 文件中:
    def global():
        pass 
    
    class Util:
        @staticmethod
        def helper():
            pass
    
    # 在 module2.py 文件中:
    import module1
    module1.global()        # 调用全局函数
    module1.Util.helper()   # 调用静态函数

    从这个角度看,定义在类中的静态函数不仅不具备命名空间的优点,甚至调用语法还更加啰嗦。对此,我的理解是:

    态函数可以被继承、重写,但全局函数不行,由于 Python 中的函数是一等公民,因此很多时候用函数替代类都会使代码

    更加简洁,但缺点就是无法继承,后面还会有更多这样的例子。

    3.2.2 属性 attribute

    Python (等多数动态语言)中的类并不像 C/OC/Java 这些静态语言一样,需要预先定义属性。我们可以直接在初始化

    函数中创建属性:

    class Person:
        def __init__(self, name):
            self.name = name
        
    bs = Person('bestswifter')
    bs.name  # 值是 'bestswifter'
    由于 __init__ 函数是运行时调用的,所以我们可以直接给对象添加属性:
    
    bs.age = 22
    bs.age  # 因为刚刚赋值了,所以现在取到的值是 22

    如果访问一个不存在的属性,将会抛出异常。从以上特性来看,对象其实和字典非常相似,但这种过于灵活的特性其实蕴含了潜在的

    风险。比如某个封装好的父类中定义了许多属性, 但是子类的使用者并不一定清楚这一点,他们很可能会不小心就重写了父类的属性

    。一种隐藏并保护属性的方式是在属性前面加上两个下划线:

    class Person:
        def __init__(self):
            self.__name = 'bestswifter'
        
    bs = Person()
    
    bs.__name          # 这样是无法获取属性的
    bs._Person__name   # 这样还是可以读取属性
    这是因为 Python 会自动处理以双下划线开头的属性,把他们重名为 _Classname__attrname 的格式。由于 Python 
    对象的所有属性都保存在实例的 __dict__ 属性中,我们可以验证一下: bs
    = Person() bs.__dict__ # 得到 {'_Person__name': 'bestswifter'}

    但很多人并不认可通过名称改写(name mangling) 的方式来存储私有属性,原因很简单,只要知道改写规则,依然很容易的就能读写

    私有属性。与其自欺欺人,不如采用更简单,更通用的方法,比如给私有属性前面加上单个下划线 _

    注意,以单个下划线开头的属性不会触发任何操作,完全靠自觉与共识。任何稍有追求的 Python 程序员,都不应该读写这些属性。

    3.2.3 特性 property

    使用过别的面向对象语言的读者应该都清楚属性的 gettersetter 函数的重要性。它们封装了属性的读写操作,

    可以添加一些额外的逻辑,比如校验新值,返回属性前做一些修饰等等。最简陋的 gettersetter 就是两个普通函数:

    class Person:
        def get_name(self):
            return self.name.upper()
            
        def set_name(self, new_name):
            if isinstance(new_name, str):
                self.name = new_name.lower()
            
        def __init__(self, name):
            self.name = name
        
    bs = Person('bestswifter')
    bs.get_name()   # 得到大写的名字: 'BESTSWIFTER'
    bs.set_name(1)  # 由于新的名字不是字符串,所以无法赋值
    bs.get_name()   # 还是老的名字: 'BESTSWIFTER'
    工作虽然完成了,但方法并不高明。在 1.2.3 节中我们就见识到了 Python 的一个特点:“内部高度封装,完全对外透明”。
    这里手动调用 getter 和 setter 方法显得有些愚蠢、啰嗦,比如对比下面的两种写法,在变量名和函数名很长的情况下,差距会更大: bs.name
    += '1995' bs.set_name(bs.get_name() + '1995') Python 提供了 @property 关键字来装饰 getter 和 setter 方法,这样的好处是可以直接使用点语法,了解 Objective-C
    的读者对这一特性一定倍感亲切:
    class Person: @property # 定义 getter def name(self): # 函数名就是点语法访问的属性名 return self._name.upper() # 现在真正的属性是 _name 了 @name.setter # 定义 setter def name(self, new_name): # 函数名不变 if isinstance(new_name, str): self._name = new_name.lower() # 把值存到私有属性 _name 里 def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 其实调用了 name 函数,得到大写的名字: 'BESTSWIFTER' bs.name = 1 # 其实调用了 name 函数,因为类型不符,无法赋值 bs.name # 还是老的名字: 'BESTSWIFTER' 我们已经在 2.4 节详细学习了装饰器,应该能意识到这里的 @property 和 @xxx.setter 都是装饰器。因此上述写法实际上等价于: class Person: def get_name(self): return self._name.upper() def set_name(self, new_name): if isinstance(new_name, str): self._name = new_name.lower() # 以上是老旧的 getter 和 setter 定义 # 如果不用 @property,可以定义一个 property 类的实例 name = property(get_name, set_name) 可见,特性的本质是给类创建了一个类属性,它是 property 类的实例,构造方法中需要把 getter、setter 等函数传入,
    我们可以打印一下类的 name 属性来证明: Person.name #
    <property object at 0x107c99868> 理解特性的工作原理至关重要。以这里的 name 特性为例,我们访问了对象的 name 属性,但是它并不存在,所以会尝试访问类
    的 name 属性,这个属性是 property 类的实例,会对读写操作做特殊处理。这也意味着,如果我们重写了类的 name 属性,
    那么对象的读写方法就不会生效了: bs
    = Person() Person.name = 'hello' bs.name # 实例并没有 name 属性,因此会访问到类的属性 name,现在的值是 'hello` 了 如果访问不存在的属性,默认会抛出异常,但如果实现了 __getattr__ 函数,还有一次挽救的机会: class Person: def __getattr__(self, attr): return 0 def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 直接访问属性 bs.age # 得到 0,这是 __getattr__ 方法提供的默认值 bs.age = 1 # 动态给属性赋值 bs.age # 得到 1,注意!!!这时候就不会再调用 __getattr__ 方法了 由于 __getattr__ 只是兜底策略,处理一些异常情况,并非每次都能被调用,所以不能把重要的业务逻辑写在这个方法中。

    3.2.4 特性工厂

    在上一节中,我们利用特性来封装 gettersetter,对外暴露统一的读写接口。但有些 gettersetter 的逻辑其实是可以复用的,比如商品的价格和剩余数量在赋值时,都必须是大于 0 的数字。这时候如果每次都要写一遍 setter,代码就显得很冗余,所以我们需要一个能批量生产特性的函数。由于我们已经知道了特性是 property 类的实例,而且是类的属性,所以代码可以这样写:

    
    
    def quantity(storage_name):  # 定义 getter 和 setter
        def qty_getter(instance):
            return instance.__dict__[storage_name]
        def qty_setter(instance, value):
            if value > 0:
                # 把值保存在实例的 __dict__ 字典中
                instance.__dict__[storage_name] = value 
            else:
                raise ValueError('value must be > 0')
        return property(qty_getter, qty_setter) # 返回 property 的实例
    
    

    有了这个特性工厂,我们可以这样来定义特性:

    
    
    class Item:
        price = quantity('price')
        number = quantity('number')
        
        def __init__(self):
            pass
                    
    i = Item()
    i.price = -1 
    # Traceback (most recent call last):
    # ...
    # ValueError: value must be > 0

    作为追求简洁的程序员,我们不禁会问,在 price = quantity('price') 这行代码中,属性名重复了两次,能不能在 quantity 函数中自动读取左边的属性名呢,这样代码就可以简化成 price = quantity() 了。

    答案显然是否定的,因为右边的函数先被调用,然后才能把结果赋值给左边的变量。不过我们可以采用迂回策略,变相的实现上面的需求:

    
    
    def quantity():
        try:
            quantity.count += 1
        except AttributeError:
            quantity.count = 0
        
        storage_name = '_{}:{}'.format('quantity', quantity.count)  
            
        def qty_getter(instance):
            return instance.__dict__[storage_name]
        def qty_setter(instance, value):
            if value > 0:
                instance.__dict__[storage_name] = value
            else:
                raise ValueError('value must be > 0')
        return property(qty_getter, qty_setter)

    这段代码中我们利用了两个技巧。首先函数是一等公民, 所以函数也是对象,自然就有属性。所以我们利用 try ... except 很容易的就给函数工厂添加了一个计数器对象 count,它每次调用都会增加,然后再拼接成存储时用的键 storage_name ,并且可以保证不同 property 实例的存储键名各不相同。

    其次,storage_namegettersetter 函数中都被引用到,而这两个函数又被 property 的实例引用,所以 storage_name 会因为被持有而延长生命周期。这也正是闭包的一大特性:能够捕获自由变量并延长它的生命周期和作用域。

    我们来验证一下:

    
    
    class Item:
        price = quantity()
        number = quantity()
        
        def __init__(self):
            pass
            
    i = Item()
    i.price = 1
    i.number = 2
    i.price     # 得到 1,可以正常访问
    i.number    # 得到 2,可以正常访问
    i.__dict__  # {'_quantity:0': 1, '_quantity:1': 2}
    可见现在存储的键名可以被正确地自动生成。

    3.2.5 属性描述符

    文件描述符的作用和特性工厂一样,都是为了批量的应用特性。它的写法也和特性工厂非常类似:

    
    
    class Quantity:
        def __init__(self, storage_name):
            self.storage = storage_name
        def __get__(self, instance, owner):
            return instance.__dict__[self.storage]
        def __set__(self, instance, value):
            if value > 0:
                instance.__dict__[self.storage] = value
            else:
                raise ValueError('value must be > 0')

    主要有以下几个改动:

    1. 不用返回 property 类的实例了,因此 gettersetter 方法的名字是固定的,这样才能满足协议。
    2. __get__ 方法的第一个参数是描述符类 Quantity 的实例,第二个参数 self 是要读取属性的实例,比如上面的 i,也被称作托管实例。第三个参数是托管类,也就是 Item
    3. __set__ 方法的前两个参数含义类似,第三个则是要读取的属性名,比如 price

    和特性工厂类似,属性描述符也可以实现 storage_name 的自动生成,这里就不重复代码了。看起来属性描述符和特性工厂几乎一样,但由于属性描述符是类,它就可以继承。比如这里的 Quantity 描述符有两个功能:自动存储和值的校验。自动存储是一个非常通用的逻辑,而值的校验是可变的业务逻辑,所以我们可以先定义一个 AutoStorage 描述符来实现自动存储功能,然后留下一个空的 validate 函数交给子类去重写。

    而特性工厂作为函数,自然就没有上述功能,这两者的区别类似于 3.2.1 节中介绍的静态函数与全局函数的区别。

    3.2.6 实例属性的查找顺序

    我们知道类的属性都会存储在 __dict__ 字典中,即使没有显式的给属性赋值,但只要字典里面有这个字段,也是可以读取到的:

    
    
    class Person:
        pass
    
    p = Person()
    p.__dict__['name'] = 'bestswifter'
    p.name  # 不会报错,而是返回字典中的值,'bestswifter'

    但我们在特性工厂和属性描述符的实现中,都是直接把属性的值存储在 __dict__ 中,而且键就是属性名。之前我们还介绍过,特性的工作原理是没有直接访问实例的属性,而是读取了 property 的实例。那直接把值存在 __dict__ 中,会不会导致特性失效,直接访问到原始内容呢?从之前的实践结果来看,答案是否定的,要解释这个问题,我们需要搞明白访问实例属性的查找顺序。

    假设有这么一段代码:

    o = cls()   # 假设 o 是 cls 类的实例
    o.attr      # 试图访问 o 的属性 attr
    再对上一节中的属性描述符做一个简单的分类:
    
    覆盖型描述符:定义了 __set__ 方法的描述符
    非覆盖型描述符:没有定义 __set__ 方法的描述符
    在执行 o.attr 时,查找顺序如下:
    
    如果 attr 出现在 cls 或父类的 __dict__ 中,且 attr 是覆盖型描述符,那么调用 __get__ 方法。
    否则,如果 attr 出现在 o 的__dict__ 中,返回 o.__dict__[attr]
    否则,如果attr 出现在 cls 或父类的 __dict__ 中,如果 attr 是非覆盖型描述符,那么调用 __get__ 方法。
    否则,如果没有非覆盖型描述符,直接返回 cls.__dict__[attr]
    否则,如果 cls 实现了 __getattr__ 方法,调用这个方法
    抛出 AttributeError

    所以,在访问类的属性时,覆盖型描述符的优先级是高于直接存储在 __dict__ 中的值的。

    3.3 多继承

    本节内容部分摘自我的这篇文章:从 Swift 的面向协议编程说开去,本节聊的是多继承在 Python 中的知识,如果想阅读关于多继承的讨论,请参考原文。

    3.3.1 多继承的必要性

    很多语言类的书籍都会介绍,多继承是个危险的行为。诚然,狭义上的多继承在绝大多数情况下都是不合理的。这里所谓的 “狭义”,指的是一个类拥有多个父类。我们要明确一个概念:继承的目的不是代码复用,而是声明一种 is a 的关系,代码复用只是 is a 关系的一种外在表现。

    因此,如果你需要狭义上的多继承,还是应该先问问自己,真的存在这么多 is a 的关系么?你是需要声明这种关系,还是为了代码复用。如果是后者,有很多更优雅的解决方案,因为多继承的一个直接问题就是菱形问题(Diamond Problem)。

    但是广义上的多继承是必须的,不能因为害怕多继承的问题就忽略多继承的优点。广义多继承 指的是通过定义接口(Interface)以及接口方法的默认实现,形成“一个父类,多个接口”的模式,最终实现代码的复用。当然,不是每个语言都有接口的概念,比如 Python 里面叫 Mixin,会在 3.3.3 节中介绍。

    广义上的多继承非常常见,有一些教科书式的例子,比如动物可以按照哺乳动物,爬行动物等分类,也可以按照有没有翅膀来分类。某一个具体的动物可能满足上述好几类。在实际的开发中也到处都是广义多继承的使用场景,比如 iOS 或者安卓开发中,系统控件的父类都是固定的,如果想让他们复用别的父类的代码,就会比较麻烦。

    
    
  • 相关阅读:
    fastx_toolkit软件使用说明
    转:bwa的使用方法
    转: Annovar 软件注释流程介绍
    转:linux下bwa和samtools的安装与使用
    GFF3格式
    漂浮广告窗
    CPU指令集设计RISC和CISC
    南方
    成都 夹三品
    微嵌1
  • 原文地址:https://www.cnblogs.com/LiLihongqiang/p/7986788.html
Copyright © 2020-2023  润新知