Python中级使用

函数

  • 调用函数

    • 参数可以多个
    >>> max(1, 2)
    2
    >>> max(2, 3, 1, -5)
    3
    
    • 参数类型需要一致
    >>> abs('a')
    TypeError: bad operand type for abs(): 'str'
    
    • 函数调用
    >>> abs(100)
    100
    >>> abs(-20)
    20
    
    • 数据类型转换

      >>> int('123')
      123
      >>> int(12.34)
      12
      >>> float('12.34')
      12.34
      >>> str(1.23)
      '1.23'
      >>> str(100)
      '100'
      >>> bool(1)
      True
      >>> bool('')
      False
      
    • 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量

      >>> a = abs # 变量a指向abs函数
      >>> a(-1) # 所以也可以通过a调用abs函数
      1
      
  • 定义函数

    • 要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回

      • def my_abs(x):
            if x >= 0:
                return x
            else:
                return -x
        
        print(my_abs(-99))
        
    • 空函数:定义一个什么事也不做的空函数,可以用pass语句

      • def nop():
            pass
        
      • pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来

    • 返回多个值

      • 需要从一个点移动到另一个点,给出坐标、位移和角度,就计算出新的坐标:

        import math
        
        def move(x, y, step, angle=0):
            nx = x + step * math.cos(angle)
            ny = y - step * math.sin(angle)
            return nx, ny
        
  • 函数参数

    • 普通形式

      • def power(x, n):
            s = 1
            while n > 0:
                n = n - 1
                s = s * x
            return s
        
    • 默认参数:降低调用函数的难度

      • def power(x, n=2):
            s = 1
            while n > 0:
                n = n - 1
                s = s * x
            return s
        
        >>> power(5)
        25
        
      • 一是必选参数在前,默认参数在后,否则Python的解释器会报错

      • 二是如何设置默认参数

        • 多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数
      • 调用

        • 当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll('Adam', 'M', city='Tianjin')
        • 按顺序提供默认参数,比如调用enroll('Bob', 'M', 7),意思是,除了namegender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值
    • 默认参数的常见陷阱

      • 定义一个函数,传入一个list,添加一个END再返回:

        def add_end(L=[]):
            L.append('END')
            return L
        
      • 使用默认参数调用时,一开始结果也是对的:

        >>> add_end()
        ['END']
        

        但是,再次调用add_end()时,结果就不对了:

        >>> add_end()
        ['END', 'END']
        >>> add_end()
        ['END', 'END', 'END']
        
      • 原因解释如下:

        Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]

      • 特别注意

      定义默认参数要牢记一点:默认参数必须指向不变对象!

      def add_end(L=None):
          if L is None:
              L = []
          L.append('END')
          return L
      
      >>> add_end()
      ['END']
      >>> add_end()
      ['END']
      
    • 可变参数:允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple

      • def calc(*numbers):
            sum = 0
            for n in numbers:
                sum = sum + n * n
            return sum
        

        在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple

        >>> calc(1, 2)
        5
        >>> calc()
        0
        
      • 已经有一个list或者tuple,要调用一个可变参数

        >>> nums = [1, 2, 3]
        >>> calc(*nums)
        14
        

        *nums表示把nums这个list的所有元素作为可变参数传进去

    • 关键字参数

      • 允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict

        def person(name, age, **kw):
            print('name:', name, 'age:', age, 'other:', kw)
        
        >>> person('Michael', 30)
        name: Michael age: 30 other: {}
        >>> person('Adam', 45, gender='M', job='Engineer')
        name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
        
        >>> extra = {'city': 'Beijing', 'job': 'Engineer'}
        >>> person('Jack', 24, **extra)
        name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
        

        **extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

    • 命名关键字参数

      • 限制关键字参数的名字,就可以用命名关键字参数

        def person(name, age, *, city, job):
            print(name, age, city, job)
        

        命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

        调用方式如下:

        >>> person('Jack', 24, city='Beijing', job='Engineer')
        Jack 24 Beijing Engineer
        

        函数定义中有一可变参数,后面的命名关键字参数就不需要一个特殊分隔符*

        def person(name, age, *args, city, job):
            print(name, age, args, city, job)
        

        命名关键字参数必传入参数名,和位置参数不同。没有传入参数名,调用报错:

        >>> person('Jack', 24, 'Beijing', 'Engineer')
        TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job
        
      • 命名关键字参数可以有缺省值,从而简化调用:

        def person(name, age, *, city='Beijing', job):
            print(name, age, city, job)
        

        由于命名关键字参数city具有默认值,调用时,可不传入city参数:

        >>> person('Jack', 24, job='Engineer')
        Jack 24 Beijing Engineer
        
    • 参数组合

      • 顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数
  • 递归函数:一个函数在内部调用自身本身

    def fact(n):
        if n==1:
            return 1
        return n * fact(n - 1)
    

高级特性:减少代码量

  • 切片

    • 用于list及其变种,进行连续指定索引的操作[前索引:后索引:间隔]

    • 取指定索引范围的操作

      >>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
      
      • >>> L[0:3]
        ['Michael', 'Sarah', 'Tracy']
        

        L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3

      • >>> L[:3]
        ['Michael', 'Sarah', 'Tracy']
        

        第一个索引是0,还可以省略

      • >>> L[-2:]
        ['Bob', 'Jack']
        >>> L[-2:-1]
        ['Bob']
        

        同样支持倒数切片,倒数第一个元素的索引是-1

      • >>> L[:10:2]
        [0, 2, 4, 6, 8]
        

        前10个数,每两个取一个

      • >>> (0, 1, 2, 3, 4, 5)[:3]
        (0, 1, 2)
        

        tuple也可以用切片操作,只是操作的结果仍是tuple

      • >>> 'ABCDEFG'[:3]
        'ABC'
        >>> 'ABCDEFG'[::2]
        'ACEG'
        

        字符串'xxx'也可以看成是一种list,每个元素就是一个字符

  • 迭代

    • 通过for ... in循环遍历

      • 比如dict就可以迭代:

        >>> d = {'a': 1, 'b': 2, 'c': 3}
        >>> for key in d:
        ...     print(key)
        ...
        a
        c
        b
        
      • 字符串也是可迭代对象,可以作用于for循环:

        >>> for ch in 'ABC':
        ...     print(ch)
        ...
        A
        B
        C
        
    • 判断是否可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:

      • >>> from collections.abc import Iterable
        >>> isinstance('abc', Iterable) # str是否可迭代
        True
        >>> isinstance([1,2,3], Iterable) # list是否可迭代
        True
        >>> isinstance(123, Iterable) # 整数是否可迭代
        False
        
      • Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身

        >>> for i, value in enumerate(['A', 'B', 'C']):
        ...     print(i, value)
        ...
        0 A
        1 B
        2 C
        
      • for循环里,同时引用了两个变量,比如下面的代码:

        >>> for x, y in [(1, 1), (2, 4), (3, 9)]:
        ...     print(x, y)
        ...
        1 1
        2 4
        3 9
        
  • 列表生成式

    • Python内置的非常简单却强大的可以用来创建list的生成式

      • 列表生成式则可以用一行语句代替循环生成上面的list:

        >>> [x * x for x in range(1, 11)]
        [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
        
      • 列表生成式也可以使用两个变量来生成list:

        >>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
        >>> [k + '=' + v for k, v in d.items()]
        ['y=B', 'x=A', 'z=C']
        
      • 使用两层循环,可以生成全排列:

        >>> [m + n for m in 'ABC' for n in 'XYZ']
        ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
        
      • for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

        >>> [x for x in range(1, 11) if x % 2 == 0]
        [2, 4, 6, 8, 10]
        
      • 在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else

        >>> [x if x % 2 == 0 else -x for x in range(1, 11)]
        [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
        
  • 生成器

    • 列表生成式,创建一个列表。受到内存限制,列表容量是有限。创建包含100万个元素列表,占用很大的存储,如果仅仅访问前面几个元素,绝大多数元素占用空间浪费

    • Python中,这种一边循环一边计算的机制,称为生成器:generator;generator保存的是算法

    • 创建一个generator

      • 只要把一个列表生成式的[]改成(),就创建了一个generator第一种方法

        >>> L = [x * x for x in range(10)]
        >>> L
        [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
        >>> g = (x * x for x in range(10))
        
        >>> next(g)
        0
        >>> next(g)
        1
        >>> next(g)
        4
        
        或者
        >>> for n in g:
        ...     print(n)
        ... 
        0
        1
        4
        9
        16
        25
        36
        49
        64
        81
        
      • fib函数变成generator函数,只需要把print(b)改为yield b就可以第二种

        以斐波那契数列为例

        def fib(max):
            n, a, b = 0, 0, 1
            while n < max:
                print(b)
                a, b = b, a + b
                n = n + 1
            return 'done'
        

        改为

        def fib(max):
            n, a, b = 0, 0, 1
            while n < max:
                yield b
                a, b = b, a + b
                n = n + 1
            return 'done'
        

        理解

        普通函数是顺序执行,遇到return语句或者最后一行函数语句就返回。

        generator函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行

    • 请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator

      • eg:先错误后正确

      • def odd():
            print('step 1')
            yield 1
            print('step 2')
            yield(3)
            print('step 3')
            yield(5)
        
        >>> next(odd())
        step 1
        1
        >>> next(odd())
        step 1
        1
        >>> next(odd())
        step 1
        1
        
        >>> g = odd()
        >>> next(g)
        step 1
        1
        >>> next(g)
        step 2
        3
        >>> next(g)
        step 3
        5
        
  • 迭代器

    • 可以直接作用于for循环的数据类型有以下几种:

      一类是集合数据类型,如listtupledictsetstr等;

      一类是generator,包括生成器和带yield的generator function。

      这些可以直接作用于for循环的对象统称为可迭代对象

      • isinstance()判断一个对象是否是**Iterable**对象:
      >>> from collections.abc import Iterable
      >>> isinstance([], Iterable)
      True
      >>> isinstance({}, Iterable)
      True
      >>> isinstance('abc', Iterable)
      True
      >>> isinstance((x for x in range(10)), Iterable)
      True
      >>> isinstance(100, Iterable)
      False
      
    • 使用isinstance()判断一个对象是否是**Iterator**对象:

      >>> from collections.abc import Iterator
      >>> isinstance((x for x in range(10)), Iterator)
      True
      >>> isinstance([], Iterator)
      False
      >>> isinstance({}, Iterator)
      False
      >>> isinstance('abc', Iterator)
      False
      

      可作用于next()函数的对象都是Iterator类型

      • listdictstrIterable变成Iterator可以使用iter()函数:

        >>> isinstance(iter([]), Iterator)
        True
        >>> isinstance(iter('abc'), Iterator)
        True
        
    • 总结

      • 集合数据类型如listdictstr等是Iterable但不是Iterator

函数式编程

  • 特点:允许把函数本身作为参数传入另一个函数,还允许返回一个函数

  • 高阶函数

    • 特点:变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数

    • 变量指向函数

      • >>> f = abs
        >>> f(-10)
        10
        
      • 变量f已经指向abs函数本身。调用abs()函数和调用变量f()完全相同

    • 函数名也是变量

      • 函数名就是指向函数的变量

      • abs()函数,可以把函数名abs看成变量,指向可以计算绝对值的函数

        >>> abs = 10
        >>> abs(-10)
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        TypeError: 'int' object is not callable
        
      • 实际代码不是这样写,只为说明

    • 高阶函数总结举例

      • def add(x, y, f):
            return f(x) + f(y)
        
        print(add(-5, 6, abs))
        
    • map()reduce()函数

      • map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回

        >>> def f(x):
        ...     return x * x
        ...
        >>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
        >>> list(r)
        [1, 4, 9, 16, 25, 36, 49, 64, 81]
        
      • reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算

        reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
        
        >>> from functools import reduce
        >>> def add(x, y):
        ...     return x + y
        ...
        >>> reduce(add, [1, 3, 5, 7, 9])
        25
        
  • 返回函数

    • 特点:把函数作为结果值返回

    • eg:如果不需要立刻求和,可以不返回求和的结果,而是返回求和的函数

      • def lazy_sum(*args):
            def sum():
                ax = 0
                for n in args:
                    ax = ax + n
                return ax
            return sum
        

        我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

        >>> f = lazy_sum(1, 3, 5, 7, 9)
        >>> f
        <function lazy_sum.<locals>.sum at 0x101c6ed90>
        

        调用函数f时,才真正计算求和的结果:

        >>> f()
        25
        

        调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

        >>> f1 = lazy_sum(1, 3, 5, 7, 9)
        >>> f2 = lazy_sum(1, 3, 5, 7, 9)
        >>> f1==f2
        False
        
    • 闭包

      • 定义:返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用
      • 注意:返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量
  • 匿名函数

    • 定义:传入函数时,有些时候,不需要显式地定义函数

    • 使用:

      • 关键字lambda表示匿名函数,冒号前面的x表示函数参数

        >>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
        [1, 4, 9, 16, 25, 36, 49, 64, 81]
        

        匿名函数lambda x: x * x实际上就是:

        def f(x):
            return x * x
        

        限制:只能有一个表达式,不用写return,返回值是该表达式的结果

      • 也是一个函数对象,可以把匿名函数赋值给一个变量,再利用变量来调用该函数

        >>> f = lambda x: x * x
        >>> f
        <function <lambda> at 0x101c6ef28>
        >>> f(5)
        25
        
      • 可以把匿名函数作为返回值返回

        def build(x, y):
            return lambda: x * x + y * y
        
  • 装饰器Decorator

    • 定义:这种在代码运行期间动态增加功能的方式

    • 函数是对象,函数对象可被赋值给变量,所以,变量能调用该函数

      • >>> def now():
        ...     print('2024-6-1')
        ...
        >>> f = now
        >>> f()
        2024-6-1
        

        函数对象有__name__属性(注意:前后各两个下划线),可拿到函数的名字:

        >>> now.__name__
        'now'
        >>> f.__name__
        'now'
        
      • decorator是返回函数的高阶函数。所以,要定义一个能打印日志的decorator

        def log(func):
            def wrapper(*args, **kw):
                print('call %s():' % func.__name__)
                return func(*args, **kw)
            return wrapper
        

        上面的log,它是一个decorator,接受一个函数作为参数,并返回一个函数

        @log
        def now():
            print('2024-6-1')
        

        调用now()函数,不仅运行now()函数本身,还运行now()函数前打印一行日志:

        >>> now()
        call now():
        2024-6-1
        
  • 偏函数

    • 定义:functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2

    • 举例:

      • >>> import functools
        >>> int2 = functools.partial(int, base=2)
        >>> int2('1000000')
        64
        >>> int2('1010101')
        85
        

        functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单

      • 最后,创建偏函数时,实际可以接收函数对象、***args**kw**这3个参数

        int2 = functools.partial(int, base=2)
        

        实际上固定了int()函数的关键字参数base,也就是:

        int2('10010')
        

        相当于:

        kw = { 'base': 2 }
        int('10010', **kw)
        

        当传入:

        max2 = functools.partial(max, 10)
        

        实际上会把10作为*args的一部分自动加到左边,也就是:

        max2(5, 6, 7)
        

        相当于:

        args = (10, 5, 6, 7)
        max(*args)
        

        结果为10

模块

  • 在python中,一个.py文件就称之为一个模块(Module)

    • 一个abc.py的文件就是一个名字叫abc的模块

    • 每一个包目录下面都有一个__init__.py的文件,这个文件必须存在,否则,Python把这个目录当普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany

      mycompany
      ├─ __init__.py
      ├─ abc.py
      └─ xyz.py
      
  • 使用模块

    • 编写一个hello的模块

      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      
      ' a test module '
      
      __author__ = 'Michael Liao'
      
      import sys
      
      def test():
          args = sys.argv
          if len(args)==1:
              print('Hello, world!')
          elif len(args)==2:
              print('Hello, %s!' % args[1])
          else:
              print('Too many arguments!')
      
      if __name__=='__main__':
          test()
      
      • 第1行和第2行是标准注释

        • 第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行
        • 第2行注释表示.py文件本身使用标准UTF-8编码
      • 第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;

      • 第6行使用__author__变量把作者写进去

      • 以上六行就是Python模块的标准文件模板,当然也可以全部删掉不写

      • 使用sys模块的第一步,就是导入该模块:

        import sys
        
      • sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:

        运行python3 hello.py获得的sys.argv就是['hello.py']

        运行python3 hello.py Michael获得sys.argv就是['hello.py', 'Michael']

      • if __name__=='__main__':
            test()
        

        当在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码

        $ python3 hello.py
        Hello, world!
        $ python hello.py Michael
        Hello, Michael!
        
      • $ python3
        Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 23 2015, 02:52:03) 
        [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
        Type "help", "copyright", "credits" or "license" for more information.
        >>> import hello
        >>>
        

        导入时,没有打印Hello, word!,因为没有执行test()函数。

        调用hello.test()时,才能打印出Hello, word!

        >>> hello.test()
        Hello, world!
        
    • 作用域

      • 一个模块中,可能会定义很多函数和变量,但有的函数和变量希望给别人使用,有的函数和变量希望仅仅在模块内部使用
      • 正常的函数和变量名是公开(public),可以被直接引用,比如:abcx123PI
      • 类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author____name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,自己的变量一般不要用这种变量名
      • 类似_xxx__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc__abc
        • private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量
      • 外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public
  • 安装第三方模块

    • pip install 你想要安装的第三方库