Python 一等函数

在 Python 中,不仅整数、字符串、字典是一等对象,连函数也被当做一等公民。这说明了什么问题,先来看看一等对象的定义:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

那么,也就意味着 Python 函数是对象,是 function 类的实例。这篇文章从函数的属性、参数等几个方面拆解并分析 Python 函数。

Python 的可调用对象

首先看看 Python 中可直接调用的对象。我们知道,调用函数的方法是用调用运算符 () ,那么判断一个对象是否可调用,就是判断 () 能否应用到该对象上,可以使用内置的 callable() 函数。

Python 数据模型文档列出了 7 种可调用对象:用户定义的函数、内置函数、内置方法、方法、类、类的实例、生成器函数。

上述 7 种对象中,函数天生就是可以调用的,方法是类中定义的函数,而创建类的实例就是直接调用了类(其实是运行类的 __new__ 方法,然后运行 __init__ 方法初始化实例)。比较难理解的是类的实例,类的实例是通常意义上的对象,对象可调用的前提是类中定义了 __call__ 方法。看个 🌰 吧:

class Father:
    def __init__(self, age):
        self.sex = "male"
        self.age = age
        self.money = 0

    def own_money(self, money):
        self.money = money


if __name__ == "__main__":
    f = Father(30)
    f.own_money(100)
    print("Father's money: {}"
          .format(f()))

运行结果:

Traceback (most recent call last):
  File "/Users/zww/my_demo/test.py", line 15, in <module>
    .format(f()))
TypeError: 'Father' object is not callable

提示 Father object 不可被调用。在 Father 类中定义下面这个方法:

    def __call__(self, *args, **kwargs):
        return self.money

运行结果:

Father's money: 100

所以,只要定义了方法 __call__ ,任何 Python 对象都可以表现得像函数。

函数的属性

我们可以通过 dir 函数来探知 Python 函数具有哪些属性:

In [1]: def fun():
   ...:     pass
   ...: 

In [2]: dir(fun)
Out[2]: 
['__call__',
 '__class__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__delattr__',
 '__dict__',
 '__doc__',
 '__format__',
 '__get__',
 '__getattribute__',
 '__globals__',
 '__hash__',
 '__init__',
 '__module__',
 '__name__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'func_closure',
 'func_code',
 'func_defaults',
 'func_dict',
 'func_doc',
 'func_globals',
 'func_name']

其中,大部分属性是 Python 对象共有的,但这里我们主要讨论几种类的实例没有,但函数有的属性:

名称 类型 说明
__annotations__ dict 参数和返回值的注解
__call__ method-wrapper 实现()运算符,即可调用对象协议
__closure__ tuple 函数闭包,即自由变量的绑定(通常是 None)
__code__ code 编译成字节码的函数元数据和函数定义体
__defaults__ tuple 形式参数的默认值
__get__ method-wrapper 实现只读描述符协议
__globals__ dict 函数所在模块中的全局变量
__kwdefaults__ dict 仅限关键字形式参数的默认值
__name__ str 函数名称
__qualname__ str 函数的限定名称,如 Random.choice

函数的参数

Python 比较强大的特性之一是提供了非常灵活的参数处理机制。而 Python3 进一步提供了仅限关键字参数。

仅限关键字参数

在 Python 中调用函数时使用 *** “展开”可迭代对象,映射到单个参数。 * 表示将调用时的多个参数放入元组中,而 ** 表示将关键字参数放入一个字典中。

用 🌰 说话:

def father_own_money(name, *args, **kwargs):
    father_name = name
    default_money = args
    owned_money = kwargs["money"]
    return father_name, default_money, owned_money


if __name__ == "__main__":
    name = "MaYun"
    result = father_own_money(name, 1, 2, money=100)
    father_name = result[0]
    default_money = result[1]
    owned_money = result[2]
    print("Father {} already has money {}.\n"
          "Then he owned money {}.".format
          (father_name, default_money, owned_money))

运行结果:

Father MaYun already has money (1, 2).
Then he owned money 100.

上面 🌰 中的函数还可以这样调用:

name = "MaYun"
arrs = {"money": 100}
result = father_own_money(name, 1, 2, **arrs)

当想指定仅限关键字参数时,可以将指定的关键字参数放在 * 后面,比如:

def father_own_money(name, *, money):
    father_name = name
    owned_money = money
    return father_name, owned_money


if __name__ == "__main__":
    name = "MaYun"
    result = father_own_money(name, money=100)
    father_name = result[0]
    owned_money = result[1]
    print("Father {} owned money {}.".
          format(father_name, owned_money))

运行结果:

Father MaYun owned money 100.

函数参数的信息

接下来我们看看函数参数的相关信息,来更好的理解函数。

结合函数的属性一节,我们知道函数的 __defaults__ 属性存放着函数形参的默认值而 __kwdefaults__ 属性则存放的是仅限关键字参数的默认值;__name__ 属性存放函数的名称;__code__ 属性存放的是编译成字节码的函数元数据和函数定义体,(好吧看不懂),没关系,只需要知道 __code__ 是一个 code 对象引用,和自身的两个属性:co_varnamesco_argcount 就足够装 B 了。

结合 🌰 做简单说明:

def father_own_money(name="MaYun", *, money=100):
    father_name = name
    owned_money = money
    return father_name, owned_money


if __name__ == "__main__":
    print("Function's name: \n\t{}".
          format(father_own_money.__name__))
    print("Function keywords defaults: \n\t{}".
          format(father_own_money.__kwdefaults__))
    print("Function formal params defaults: \n\t{}".
          format(father_own_money.__defaults__))
    print("Function __code__ property: \n\t{}".
          format(father_own_money.__code__))
    print("Function all params name: \n\t{}".
          format(father_own_money.__code__.co_varnames))
    print("Function formal params num: \n\t{}".
          format(father_own_money.__code__.co_argcount))

先看运行结果:

Function's name: 
    father_own_money
Function keywords defaults: 
    {'money': 100}
Function formal params defaults: 
    ('MaYun',)
Function __code__ property: 
    <code object father_own_money at 0x101f618a0, file "xx.py", line 14>
Function all params name: 
    ('name', 'money', 'father_name', 'owned_money')
Function formal params num: 
    1

结合运行结果可以看出 __code__.co_varnames 记录的是该函数的所有参数,包括传参和函数内定义的局部变量;而 __code__.co_argcount 记录的是形式参数的个数,可以看出不包含关键字参数。更多关于函数参数的分析可以引用 Python 的 inspect 模块来提取函数的前面,感兴趣可以研究研究,这里不做详解。

函数注解

Python3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据,就是函数注解,目的是更方便的进行文档编写、参数检查等。这是个可选项,入参的注解在参数后加个 : 即可,而返回值的注解需要在 ): 直接添加 -> 和一个表达式。上面 father_own_money 添加注解如下:

def father_own_money(name: str ="MaYun", *, money: "default = 100"=100) -> ():
    father_name = name
    owned_money = money
    return father_name, owned_money

需要说明的是,函数注解仅仅在函数的 __annotations__ 属性中做了存储,除此之外 Python 不做检查、不做验证、不做强制,没有任何处理。换句话说,注解只是元数据,可以供 IDE 、框架和装饰器等工具使用。

高阶函数和匿名函数

了解了 Python 函数的以上特性之后,我们就可以利用一等函数的特性实现函数式风格编程了。

高阶函数

函数式风格编程的特点之一就是高阶函数,那么什么是高阶函数呢?既然 Python 函数是一等公民,那么 TA 既可以作为函数参数传入,也可以作为结果返回。而接受函数为参数,或者把函数作为结果返回的函数,我们称之为高阶函数。Python 中最为人熟知的高阶函数有:map()reduce()sorted()filter()

  • map(func, *iterables) --> map object

map() 函数的传参:函数和一系列可迭代对象;返回一个 map 对象;函数的操作是将第二个参数(可迭代对象)的每个元素代入第一个参数(函数)中,得到的结果作为函数返回值。 🌰 如下:

def father_own_money(name: str = "MaYun", money=100):
    father_name = name
    owned_money = money
    return father_name, owned_money


if __name__ == "__main__":
    name = ["mayun"] * 10
    money = range(0, 10)
    results = map(father_own_money, name, money)
    print(list(results))

运行结果:

[('mayun', 0), ('mayun', 1), ('mayun', 2), ('mayun', 3), ('mayun', 4), ('mayun', 5), ('mayun', 6), ('mayun', 7), ('mayun', 8), ('mayun', 9)]

需要说明的是, map() 传参中,可迭代对象有几个,取决于第一个函数有多少参数,用上面的 🌰 说就是 namemoney 分别作为 father_own_money 的第一和第二个参数,可迭代数量按最少的算。由于返回值 results 是个 map 对象,需要 list 一下,才能得到所有的返回结果。

  • reduce(function, sequence) -> value

map() 不同的是, reduce() 的传参 function 必须有两个参数;返回值就是 function 的返回值类型;函数的操作是:用 function 先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,直到计算完所有元素,最后得到一个结果。

def father_money_sum(default_money: int, owned_money: int):
    return default_money + owned_money


if __name__ == "__main__":
    money = range(0, 101)
    from functools import reduce

    results = reduce(father_money_sum, money)
    print("Father's all money: {}".format(results))

运行结果:

Father's all money: 5050

需要注意的是, reduce 在 Python3 中从全局名字空间中被移除了,需要先引入。

  • sorted(*args, **kwargs)

sorted 函数本身只接受一个列表来排序,但因为其可选参数 key 接受函数来应用到列表的各个元素上进行排序,所以摇身一变成了高阶函数(2333333)。看个 🌰 :

def father_money_sum(default_money: int, owned_money: int = 90):
    if default_money > 150:
        return owned_money
    return default_money + owned_money


if __name__ == "__main__":
    father_money = [100, 200, 201, 105, 189]
    result = sorted(father_money, key=father_money_sum)
    print(result)

运行结果:

[200, 201, 189, 100, 105]

由运行结果可以看出, sorted 函数仅仅将 father_money 这个列表中的每个值代入到 father_money_sum 函数中,针对结果对原列表进行排序,并没有改变列表中的值。

  • filter(function or None, iterable) --> filter object

filter() 函数的操作是将 iterable 中的元素分别代入 function 中,将结果为 True 的返回。

def father_money_sum(default_money: int, owned_money: int = 90):
    if default_money > 150:
        return None
    return default_money + owned_money


if __name__ == "__main__":
    father_money = [100, 200, 201, 105, 189]

    result = filter(father_money_sum, father_money)
    print(list(result))

匿名函数

为了更好的使用高阶函数,有时候创建一些小型的函数更为便利,这时候匿名函数的作用就体现出来了。

匿名函数主要是用关键字 lambda ,语法为:

lambda x: x + 1

上面这个 🌰 用数学表达式可以写成: f(x) = x + 1 ,这样理解就方便多了, lambda 相当于数学函数中的 f: 后的表达式的计算结果为匿名函数的返回值。这种简单的语法也意味着 lambda 函数只支持纯表达式,不能赋值不能用 while try 等 Python 语句

当然, lambda 表达式也支持多参数,比如:

lambda x, y: x + y

用数学表达式写成: f(x, y) = x + y

lambda 句法只是语法糖:与 def 语句一样,lambda 表达式会创建函数对象。

2018/05/26 17:23 下午 posted in  Python

Python 对象引用与可变性

Python 中的变量都是引用式的,这个概念很容易在写代码的时候引入 bug,还不易察觉。这篇文章就是讲述 Python 中对象的引用和可变性,然而首先要抛弃变量是存储数据的盒子的传统观念。

变量不是盒子,是标签

Python 中对变量有一个形象的比喻:变量不是盒子,是标签。也就是说变量名都是对象的标注,不是一个盒子装着对象,贴了再多的标签,对象也只有一个。

用 c++ 的思想理解起来就是:Python 中对变量的赋值都是引用传递,而不是值传递。比如:

In [1]: aa = ["a", "b", "c"]

In [2]: bb = aa

In [3]: bb.append("d")

In [4]: bb
Out[4]: ['a', 'b', 'c', 'd']

In [5]: aa
Out[5]: ['a', 'b', 'c', 'd']

is 和 ==

Python 中的 is 比较的是对象的标识,可以理解成对象的地址;而 == 比较的是对象的值,可以理解成对象指向的值。

Python 中 is 运算符比 == 速度快,这是因为 is 不能重载,Python 不必寻找并调用其他特殊方法,直接比较对象的 id ;而 == 是语法糖,调用了 __eq__ 方法,所以 a == b 等同于 a.__eq__(b)。看个🌰:

In [1]: a = [1, 2, 3]

In [2]: b = a

In [3]: c = [1, 2, 3]

In [4]: a is b
Out[4]: True

In [5]: a is c
Out[5]: False

In [6]: a == b
Out[6]: True

In [7]: a == c
Out[7]: True

In [8]: id(a)
Out[8]: 4553282696

In [9]: id(b)
Out[9]: 4553282696

In [10]: id(c)
Out[10]: 4553460264

在写代码的日常中,我们关注的大多是值,而不是标识,所以 == 的使用频率比 is 多得多。然而在变量和单例值比较时,推荐使用 is。所以在判断某变量的值是否为 None 时,推荐的写法是 a is None ,否定的写法是 a is not None

元组的相对不变性

Python 中元组的存在是以其不可变性为特征,一旦创建不可修改。但元组和其他集合一样保存的是对象的引用,也就是说虽然元组本身不可变,但若其保存的对象是可变的,元组内的元素就是可变的。所以,元组的相对不可变性指的就是,tuple 数据结构的物理内容(即保存的引用)不变,与引用的对象无关。

看个🌰就知道了:

In [1]: a = (1, [1, 2])

In [2]: b = (1, [1, 2])

In [3]: a == b
Out[3]: True

In [4]: id(a[-1])
Out[4]: 4445861072

In [5]: a[-1].append(3)

In [6]: id(a[-1])
Out[6]: 4445861072

In [7]: a == b
Out[7]: False

深拷贝和浅拷贝

在 Python 中经常有拷贝的操作,然而这就是坑开始的地方。列表复制通常使用内置的 list 方法或直接 a = b[:] ,但默认的都是浅拷贝。浅拷贝的定义是:复制了最外层容器,副本中的元素是源容器中元素的引用。也就是说副本中的元素只是给原本中的元素贴上了标签,用的都是引用传递,无论原本还是副本中的对象有了修改,都会影响另一方。

用🌰说话:

In [1]: father = [1, 2, [3, 4]]

In [2]: sun = list(father)

In [3]: father.append(5)

In [4]: father
Out[4]: [1, 2, [3, 4], 5]

In [5]: sun
Out[5]: [1, 2, [3, 4]]

In [6]: father[2].append(6)

In [7]: father
Out[7]: [1, 2, [3, 4, 6], 5]

In [8]: sun
Out[8]: [1, 2, [3, 4, 6]]

In [9]: sun.append(7)

In [10]: father
Out[10]: [1, 2, [3, 4, 6], 5]

In [11]: sun
Out[11]: [1, 2, [3, 4, 6], 7]

上面的🌰中,sunfather 的副本,但是只拷贝了 0 ~ 2 位,可以看出只有这三位的元素互相影响。

由于上面🌰中拷贝的三位元素都是可变的,我们再看一个不可变的🌰:

In [1]: father = [1, 2, (3, 4)]

In [2]: sun = list(father)

In [3]: id(father[2])
Out[1]: 4397476104

In [4]: id(sun[2])
Out[2]: 4397476104

In [5]: father[2] += (5, 6)

In [6]: id(father[2])
Out[3]: 4398347376

In [7]: id(sun[2])
Out[4]: 4397476104

In [8]: father
Out[5]: [1, 2, (3, 4, 5, 6)]

In [9]: sun
Out[6]: [1, 2, (3, 4)]

看第 5 行,元组中 += 运算符相当于创建一个新的元组,然后赋值给 father,所以这时候两个列表中第 2 位的元素不再是同一个对象。

再说深拷贝,深拷贝的定义是:副本不共享内部对象的引用。一看定义就知道,这是我们大多情况需要的,而 copy 模块中的 copydeepcopy 函数就是实现浅拷贝和深拷贝的。还用 father & sun 做🌰:

In [1]: import copy

In [2]: father = [1, 2, [3, 4]]

In [3]: sun1 = copy.copy(father)

In [4]: sun2 = copy.deepcopy(father)

In [5]: father[2].append(5)

In [6]: father
Out[6]: [1, 2, [3, 4, 5]]

In [7]: sun1
Out[7]: [1, 2, [3, 4, 5]]

In [8]: sun2
Out[8]: [1, 2, [3, 4]]

In [9]: sun2[2].append(6)

In [10]: father
Out[10]: [1, 2, [3, 4, 5]]

In [11]: sun1
Out[11]: [1, 2, [3, 4, 5]]

In [12]: sun2
Out[12]: [1, 2, [3, 4, 6]]

sun2father 的深拷贝,可以看出是一个独立的对象,不跟 father 共享任何元素。

函数的传参

再看函数的传参,c++ 中函数的传参方式分值传递、引用传递和指针传递,而 Python 中函数的传参方式只有一种:共享传参,也就是说函数内部的形参是实参的别名。

那么坑来了。

我们有时候会给函数加一个有默认值的参数,这是为了很好的向后兼容。如果不小心把这个默认值设成了可变参数,连怎么掉进坑的都不知道。看个🌰:

class Father:
    sex = "male"

    def __init__(self, name, age, money=[]):
        self._name = name
        self.__age = age
        self.money = money

class Sun(Father):
    def __init__(self, name, age):
        super(Sun, self).__init__(name, age)

if __name__ == "__main__":
    f = Father("f_name", 23)
    s = Sun("s_name", 3)
    print("father's default money: {}".format(f.money))
    f.money.append(100)
    print("father put 100 in money: {}".format(f.money))
    print("sun's default money: {}".format(s.money))
    s.money.append(10)
    print("sun put 10 in money: {}".format(s.money))
    print("now, father's money: {}".format(f.money))

上面类 Father 的 __init__ 函数的参数 money 设置了默认值 [] 。类 Sun 是继承自 Father 的,如果 father 在自己的钱包里放了 100 块钱,这时候 sun 的口袋却默认有了 100 块钱。看运行结果:

father's default money: []
father put 100 in money: [100]
sun's default money: [100]
sun put 10 in money: [100, 10]
now, father's money: [100, 10]

这样一来,爸爸和儿子的钱包就成了共享的了,很容易引发家庭矛盾。出现这个问题的根源在于,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。我们可以通过下面这段代码验证:

print(Father.__init__.__defaults__[0] is f.money)
print(f._name is s._name)
print(f.money is s.money)

运行结果:

True
False
True

因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

防坑

为了防止踩到上面这样的坑,我们需要将函数默认值设成不可变的,如 None。上面的🌰就需要改成这样:

class Father:
    sex = "male"

    def __init__(self, name, age, money=None):
        self._name = name
        self.__age = age
        if money:
            self.money = list(money)
        else:
            self.money = []


class Sun(Father):
    def __init__(self, name, age):
        super(Sun, self).__init__(name, age)

运行结果:

father's default money: []
father put 100 in money: [100]
sun's default money: []
sun put 10 in money: [10]
now, father's money: [100]

由运行结果可以看出,这时候父子两人的钱包就是独立的,互不影响了。

Python 驻留机制

不知道玩蛇的同学有没有发现一个问题,如下:

In [1]: a = 1

In [2]: b = 1

In [3]: a is b
Out[3]: True

In [4]: a = 10000

In [5]: b = 10000

In [6]: a is b
Out[6]: False

这个问题十分有趣,但是不影响平时写代码,只是 CPython 的细节实现,不知道完全没关系。

Python 有个驻留机制,即共享字符串字面量,是一种优化措施,防止重复创建热门数字。但 CPython 不会驻留所有字符串和整数,驻留的条件是实现细节,而且没有文档说明。

字符串驻留的范围:0 ~ 9A ~ Z_a ~ z
整数的驻留范围:-5 ~ 256
如果字符串或整数不在上述范围,Python 就会创建一个新的对象。

我们可以查看 id 来证明:

In [7]: a = 1

In [8]: b = 1

In [9]: id(a)
Out[9]: 140387930888536

In [10]: id(b)
Out[10]: 140387930888536

In [11]: c = 257

In [12]: d = 257

In [13]: id(c)
Out[13]: 140387934343656

In [14]: id(d)
Out[14]: 140387934343848
2018/05/24 11:53 上午 posted in  Python