深入理解Python中的函数

文章是《流畅的Python》第五章节:一等函数的学习笔记和个人理解。

1.0 前言

在Python中记住一句真理:一切皆对象。包括函数也是对象,函数可以作为参数传递给另一个函数,也可以作为另一个函数的返回结果。

接受函数为参数,或者把函数作为结果返回的函数是高阶函数 ,比如说mapfilterreducesorted等。

1.1 匿名函数

当一个函数体比较简单,又不想去想函数体的命名时就可以使用匿名函数:

Python中使用lambda关键字来构造匿名函数。

例如,使用lambda表达式反转拼写,然后依此给单词列表排序:

>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>

优点:当函数体特别简单时使用可以显得优雅。

缺点:不易看懂,所以在写lambda函数时,最好编写一个注释说明,关于lambda的使用在Python中还是存在争议的。

1.2 可调用对象

在Python中一个可调用的对象,内部一定实现了callable()函数。Python中有 7 种可调用对象:

>>> abs, str, 13
(<built-in function abs>, <class 'str'>, 13)
>>> [callable(obj) for obj in (abs, str, 13)]
[True, True, False]

用户自定义的函数

使用 def 语句或lambd 表达式创建。

内置函数

使用C语言(CPython)实现的函数,如lentime.strftime

内置方法

使用C语言实现的方法,如dict.get

方法

在类的定义体中定义的函数。

调用类时会运行类的 __new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方,所以调用类相当于调用函数。

类的实例

如果类定义了__call__方法,那么它的实例可以作为函数调用。

生成器函数

使用yield关键字的函数或方法。调用生成器函数返回的是生成器对象。

1.2.1 自定义可调用类型

在自定义类型中只要实现__call__方法就可以表现的像函数。

例如: 调用 BingoCage 实例,从打乱的列表中取出一个元素 。

import random


class BingoCage:
	# 初始化一个打乱的列表
	def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)
    # 从打乱的列表中返回一个元素,若列表为空则抛出异常
	def pick(self):
        try:
        	return self._items.pop()
        except IndexError:
        	raise LookupError('pick from empty BingoCage')
    # 使自定义类可以像方法一样使用  	
    def __call__(self):
        return self.pick()
>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True

1.3 函数内省

使用dir函数可以查看一个对象的属性。

>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__','__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__','__subclasshook__']
>>>

Python是一门动态语言,动态语言的强大之处就在于可以动态的给对象赋予属性。

例如Django管理后台的一段源代码,动态的给方法upper_case_name赋予属性:

def upper_case_name(obj):
	return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'

函数使用__dict__属性存储赋予它的用户属性。

1.4 函数的几种传参方式

Python3中的函数可以有四种接收参数的方式:位置参数可变参数命名参数命名关键字参数

一般传递参数和接收参数的顺序最好符合上边的顺序。

通过生成标签的一段代码来理解几种参数的使用方式:

  • name该位置参数是标签的名称
  • *content该可变参数用于接收标签的内容
  • cls=None该命名参数用于给标签生成class名
  • **attrs该命名关键字参数用于给标签生成属性

理解了这几个参数的作用,函数内的代码可以不用看。

def tag(name, *content, cls=None, **attrs):
    """生成一个或多个HTML标签"""
    if cls is not None:
    	attrs['class'] = cls
    if attrs:
		attr_str = ''.join(' %s="%s"' % (attr, value)
						for attr, value
						in sorted(attrs.items()))
	else:
		attr_str = ''
	if content:
		return '\n'.join('<%s%s>%s</%s>' %
							(name, attr_str, c, name) for c in content)
	else:
		return '<%s%s />' % (name, attr_str)

使用案例:

  • 只传入标签名,函数使用位置参数接收:

    >>> tag('br') 
    '<br />'
    
  • 传入标签名和标签内容,函数使用位置参数来接收标签名,使用可变参数来接收标签内容:

    >>> tag('p', 'hello')
    '<p>hello</p>'
    >>> print(tag('p', 'hello', 'world'))
    <p>hello</p>
    <p>world</p>
    
  • 传入标签名和标签内容以及id名,其中传入id名使用的是键值对的方式:

    >>> tag('p', 'hello', id=33)
    '<p id="33">hello</p>'
    

    其中id=33函数是以命名关键字参数**attrs接收的,因为只有当键值对的键名和函数的命名参数的键名一样时,命名参数才能接收该参数。

    >>> print(tag('p', 'hello', 'world', cls='sidebar'))
    <p class="sidebar">hello</p>
    <p class="sidebar">world</p>
    
  • 传入标签名和属性,有时候我们不确定函数的位置参数的位置是什么样的,我们可以以命名参数的方式传入,但是传递的参数的键必须与位置参数一样

    >>> tag(content='testing', name="img")
    '<img content="testing" />
    

    上边的content='testing'虽然放在第一个位置,但是它是命名关键字参数。

    注:但是并不提倡这种写法,最好按照函数定义的参数位置来传入参数

  • 传入标签名,标签内容,属性。不管是传递什么参数我们都可以使用键值对的方式传入:

    >>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
    ... 'src': 'sunset.jpg', 'cls': 'framed'}
    >>> tag(**my_tag)
    '<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
    

    只要在字典前加上**,字典中的所有元素作为单个参数传入,同名键会绑定到对应的命名参数和位置参数上,余下的则被**attrs接收。这其实就是Python经典的打包和解包的过程。

1.5 获取关于参数的信息

在Bobo框架(Python一个轻量级web框架)中使用装饰器来映射url, 但是却不需要指定传递的参数:

import bobo

# hello 函数需要一个person作为参数,但是装饰器却并没有传递
@bobo.query('/')
def hello(person):
	return 'Hello %s!' % person

关于Bobo的介绍与使用可以看这篇博客

那这个装饰器是如何知道需不需要传递参数,传递的参数又是什么类型呢。

在Python中函数对象有几个属性用来保存参数和变量的信息:

  • __defaults__:值是一个元组,里面保存着位置参数和命名参数(关键字参数)的默认值。
  • __kwdefaults__:保存着命名关键字参数的默认值。
  • __code__:值是一个code对象引用, 自身也有很多属性,参数的名称保存在这里。

通过一个示例来讲解:在指定长度附近截断字符串的函数:

def clip(text, max_len=80):
"""在max_len前面或后面的第一个空格处截断文本
"""
end = None
if len(text) > max_len:
	space_before = text.rfind(' ', 0, max_len)
	if space_before >= 0:
		end = space_before
	else:
        space_after = text.rfind(' ', max_len)
        if space_after >= 0:
        	end = space_after
if end is None: # 没找到空格
	end = len(text)
return text[:end].rstrip()

提取关于clip函数的参数信息:

>>> from clip import clip
>>> clip.__defaults__
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS
<code object clip at 0x...>
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
>>> clip.__code__.co_argcount
2
  • __code__.co_varnames:里面保存着参数名称和局部变量的名称,参数名称保存在前N个位置。
  • __code__.co_argcount:确定__code__.co_varnames中参数的前N个位置,需要注意的是这里不包含前缀为***的变长参数。
  • 确定了位置数量后就可以在__default__中取对应的参数的默认值,需要注意的是默认值的顺序与__code__.co_varnames参数的名称的顺序是相反的。

1.5.1 使用inspect 模块获取函数信息

前边那种获取参数的方式比较复杂,使用inspect 模块中的signature 方法可以直接获取参数信息:

>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)

# inspect.signature 函数返回一个 inspect.Signature 对象
>>> sig # doctest: +ELLIPSIS
<inspect.Signature object at 0x...>
>>> str(sig)
'(text, max_len=80)'

# 该对象有一个parameters属性是一个有序映射
# 将参数名和inspect.Parameter对应起来
# 其中inspect.Parameter有自己的属性:
# 		name、default和kind
>>> for name, param in sig.parameters.items():
... print(param.kind, ':', name, '=', param.default)
...
# inspect._empty表示None,None也是有效的默认值
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

获取关于参数的信息的作用

Python语言是解释性语言,也就是说是先将.py文件由解释器解释为.pyc的中间文件,执行程序的时候读取的是.pyc文件,执行一行将一行翻译为机器码。

像框架与IDE等工具,在将.py文件转换为.pyc文件前就会先验证函数的形参与实参之间的对应关系是否正确。

这个时候就需要获取函数的签名信息来验证了,inspect.Signature对象有个bind方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。

使用前边的标签案例:

>>> import inspect
>>> sig = inspect.signature(tag)
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'cls': 'framed'}
>>> bound_args = sig.bind(**my_tag)
>>> bound_args
<inspect.BoundArguments object at 0x...>
>>> for name, value in bound_args.arguments.items():
... print(name, '=', value)
...
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}

# 将实参去掉部分后,形参与实参不对应就会抛出异常
>>> del my_tag['name']
>>> bound_args = sig.bind(**my_tag)
Traceback (most recent call last):

1.5.2 函数注解

Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据。

注解不会做任何事情,Python 不做检查、不做强制、不做验证,什么操作都不做。只会将注解的信息保存在__annotaions__属性里,也就是说你写了注解但是没用到__annotaions__属性里的信息,那么就是白写。

有注解的 clip 函数,与前边的clip函数的唯一区别就是第一行:

def clip(text:str, max_len:'int > 0'=80) -> str:
"""在max_len前面或后面的第一个空格处截断文本
"""
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
        	end = space_before
    	else:
    		space_after = text.rfind(' ', max_len)
    if space_after >= 0:
    	end = space_after
    if end is None: # 没找到空格
    	end = len(text)
    return text[:end].rstrip()
  • 函数声明中的各个参数可以在 : 之后增加注解表达式。
  • 如果参数有默认值,注解放在参数名和 =号之间。
  • 如果想注解返回值,在)和函数声明末尾的 :之间添加 ->和一个表达式。
  • 注解中最常用的类型是类(如 strint)和字符串(如'int > 0')。在示例中, max_len 参数的注解用的是字符串。

可以通过inspect.signature()函数提取注解中的信息:

>>> from clip_annot import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig.return_annotation
<class 'str'>
# sig.parameters 属性将参数名映射到Parameter对象上。
# 每个Parameter对象自己也有annotaion 属性
>>> for param in sig.parameters.values():
... note = repr(param.annotation).ljust(13)
... print(note, ':', param.name, '=', param.default)
<class 'str'> : text = <class 'inspect._empty'>
'int > 0' : max_len = 80

函数注解的最大影响是为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息。

1.6 支持函数式编程的包

Python中使用函数式编程主要使用operatorfunctools等包。

Python中使用函数式编程主要使用operatorfunctools等包。

1.6.1 operator 包的介绍

operator 使用介绍

  • 使用reduceoperator.mul 函数计算阶乘:

    from functools import reduce
    from operator import mul
    
    def fact(n):
    	return reduce(mul, range(1, n+1))
    
  • 使用itemgetter从序列中取出元素,排序一个元组列表:

 >>> metro_data = [
 ... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
 ... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
 ... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
 ... ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
 ... ]
 >>>
 >>> from operator import itemgetter
 
 #  这里itemgetter(1) 的作用与 lambda fields: fields[1] 一样
 
 >>> for city in sorted(metro_data, key=itemgetter(1)):
 ... print(city)
 ...
 ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
 ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
 ('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
 ('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
 ('New York-Newark', 'US', 20.104, (40.808611, -74.020386))

如果多个参数传给itemgetter,它的构建函数会返回提取的值构成的元组:

>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
... print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')
>>>
  • 使用attrgetter提取对象的属性,把多个属性名传给attrgetter,返回提取的值构成的元组。

    构建两个命名元组:

    >>> from collections import namedtuple
    # 表示经纬度
    >>> LatLong = namedtuple('LatLong', 'lat long') 
    # 表示城市的地理位置信息
    >>> Metropolis = namedtuple('Metropolis', 'name cc pop coord') 
    

    城市数据:

    >>> metro_data = [
    ... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ... ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
    ... ]
    >>>
    

    利用两个命名元组和城市数据实例化一个列表,列表中存放的是命名元组:

    
    # 这里使用嵌套的元组拆包提取(lat, long)然后使用它们构建 LatLong,作为 Metropolis 的 coord 属性。
    >>> metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long))
    ... for name, cc, pop, (lat, long) in metro_data]
    >>> metro_areas[0]
    Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722,
    long=139.691667))
    >>> metro_areas[0].coord.lat 
    35.689722
    

    使用attrgetter提取对象的属性:

    >>> from operator import attrgetter
    >>> name_lat = attrgetter('name', 'coord.lat')
    >>>
    >>> for city in sorted(metro_areas, key=attrgetter('coord.lat')):
    ... print(name_lat(city))
    ...
    ('Sao Paulo', -23.547778)
    ('Mexico City', 19.433333)
    ('Delhi NCR', 28.613889)
    ('Tokyo', 35.689722)
    ('New York-Newark', 40.808611)
    
  • methodcaller创建的函数会在对象上调用参数指定的方法:

    >>> from operator import methodcaller
    >>> s = 'The time has come'
    >>> upcase = methodcaller('upper')
    >>> upcase(s)
    'THE TIME HAS COME'
    
    # 第一个参数表示方法名,后边的参数表示该方法需要接收的参数
    >>> hiphenate = methodcaller('replace', ' ', '-')
    >>> hiphenate(s)
    'The-time-has-come'
    

operator中有很多方法可以使用:

>>> [name for name in dir(operator) if not name.startswith('_')]
['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains',
'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt',
'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imod', 'imul',
'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift',
'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le',
'length_hint', 'lshift', 'lt', 'methodcaller', 'mod', 'mul', 'ne',
'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub',
'truediv', 'truth', 'xor']

1.6.2 使用functools.partial 冻结参数

functools.partial该函数接收的参数类型有两种:

  1. 其他函数名
  2. 其他函数需要的部分参数

该函数就是对其他函数的一个封装并对外提供指定的API,比如说operator中的mul用于计算乘法,在使用的时候一般都需要传递两个参数mul(a, b),但是我想固定参数a的值,又不想对外暴露我固定的值,这个时候就可以使用functools.partial函数进一步封装:

>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3)
>>> triple(7)
21
>>> list(map(triple, range(1, 10)))
[3, 6, 9, 12, 15, 18, 21, 24, 27]

继续使用1.4节的标签的例子,我现在想封装一个picture标签,这个时候就需要固定namecls这两个参数:

>>> from tagger import tag

# 原函数id
>>> tag
<function tag at 0x10206d1e0>

# 使用partial封装tag函数,固定指定参数
>>> from functools import partial
>>> picture = partial(tag, 'img', cls='pic-frame')

# 使用封装后的picture对象创建一个tag
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />'

# 查看picture对象
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', cls='pic-frame')

# 封装完后返回的picture对象调用的函数的id 和原tag函数是一样的
>>> picture.func
<function tag at 0x10206d1e0>

>>> picture.args
('img',)
>>> picture.keywords
{'cls': 'pic-frame'}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如来心秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值