Python 常用语法整理 (一)
python函数参数中的冒号与箭头
一些python函数中,参数后面有冒号,函数后面还有箭头,这是什么含义呢?
函数参数中的冒号是参数的类型建议符,告诉函数调用者希望传入的实参的类型。函数后面跟着的箭头是函数返回值的类型建议符,用来说明该函数返回的值是什么类型。
官方的解释是type hints,是Python 3.5新加的功能,作用如上所述,官方文档为 :https://www.python.org/dev/peps/pep-0484/
值得注意的是,类型建议符并非强制规定和检查,也就是说即使传入的实际参数与建议参数不符,也不会报错。类型建议符的作用更多的体现在软件工程方面:在多人合作的时候,我们对他人开发的代码并不熟悉,没有对类型的解释说明的话,往往需要花费更多的时间才能看出函数的参数和返回值是什么类型,有了说明符,可以方便程序员理解函数的输入与输出,在具体工作中,例如静态分析与代码重构,会更加高效。
下面以一个简单的函数twoSum为例,该函数计算的是两个输入参数的和:
def twoSum(num1: int, num2: int=100) -> int:
sum = num1 + num2
return sum
if __name__ == "__main__":
print(twoSum.__annotations__)
print(twoSum(10,20))
print(twoSum(1))
print(twoSum('I love ','coding'))
打印:
{'num1': <class 'int'>, 'num2': <class 'int'>, 'return': <class 'int'>}
30
101
I love coding
几点解释:
- 第一行输出中的annotations是函数的保留属性,保存的是函数声明中的注释内容,比如我们使用的对参数"num1","num2"和返回值的建议类型。
- 第二行输出是正常用法。
- 第三行输出验证了,未传入实参时,该参数获得的默认值
- 第四行输出则验证了该解释说明符并非强制检查,我们传入了两个str实参,并不会报错,而是继续进行函数中的加法运算。如果传入的两个实参无法进行函数中规定的运算,则会正常报错。
项目实战:
def drop_duplication(df: pd.DataFrame, filter_type, group_info=None) -> pd.DataFrame:
"""重复数据筛选算子
Args:
df: pd.DataFrame
filter_type: int 去重方式 1--默认全去重 2--分组去重
group_info: dict 分组去重信息 filter_type=2时需要,根据有无筛选列有两种方式
1.{"group_columns": [], "duplicate": {"sign": "first"}} sign first/last
2.{"group_columns": [], "duplicate": {"sign": "max", "column": col1}} sign max/min
Returns:
result: pd.DataFrame 去重后的数据
"""
if filter_type == EnumDropDuplicate.ALL:
return df.drop_duplicates()
elif filter_type == EnumDropDuplicate.GROUP:
group_columns = group_info["group_columns"]
duplicate_info = group_info["duplicate"]
sign = duplicate_info["sign"]
col = duplicate_info.get("column")
if col:
df = df.sort_values(col)
sign = "last" if sign == "max" else "first"
else:
raise ValueError("filter_type参数值不在【1,2】内")
return df.drop_duplicates(subset=group_columns, keep=sign)
装饰器
函数装饰器
def my_decorator(func):
@functools.wrap
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
print('hello world')
greet()
- @是语法糖@my_decorator就相当于前面的greet=my_decorator(greet)
- 一般使用装饰器后,原函数的元信息会丢失。使用@functools.wrap帮助保留原函数的元信息。
类装饰器
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)
@Count
def example():
print("hello world")
example()
# 输出
num of calls is: 1
hello world
example()
# 输出
num of calls is: 2
hello world
...
- @Count等价于example=Count(example)
- 后面继续调用example()等于调用Count的
__call__
参考代码:
import functools
def outer(origin):
@functools.wraps(origin)
def inner(*args, **kwargs):
# 我是一个装饰器函数
print("装饰器的学习")
res = origin(*args, **kwargs)
print("装饰器的学习")
return res
return inner
@outer
def c(a5, a6, a7):
# 我是个函数
print("我是三函数")
c(4, 5, 6)
print(c.__name__)
print(c.__doc__)
动态参数,*args是非关键字参数,用于元组,**kwargs是关键字参数 (字典)
基于类的视图
基于类的视图提供另一种将视图实现为 Python 对象而不是函数的方法。它们不能替代基于函数的视图,但与基于函数的视图相比,它们是有某些不同和优势的。
- 与特定的 HTTP 方法(GET, POST, 等等)关联的代码组织能通过单独的方法替代条件分支来解决。
- 面向对象技术(比如 mixins 多重继承)可用于将代码分解为可重用组件。
使用基于类的视图¶
本质上来说,基于类的视图允许你使用不同的类实例方法响应不同 HTTP 请求方法,而不是在单个视图函数里使用有条件分支的代码。
因此在视图函数里处理 HTTP GET 的代码应该像下面这样:
from django.http import HttpResponse
def my_view(request):
if request.method == 'GET':
# <view logic>
return HttpResponse('result')
而在基于类的视图里,会变成:
from django.http import HttpResponse
from django.views import View
class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')
因为 Django 的 URL 解析器期望发送请求和相关参数来调动函数而不是类,基于类的视图有一个 as_view() 类方法,当一个请求到达的 URL 被关联模式匹配时,这个类方法返回一个函数。这个函数创建一个类的实例,调用 setup() 初始化它的属性,然后调用 dispatch() 方法。 dispatch 观察请求并决定它是 GET 和 POST,等等。如果它被定义,那么依靠请求来匹配方法,否则会引发 HttpResponseNotAllowed 。
# urls.py
from django.urls import path
from myapp.views import MyView
urlpatterns = [
path('about/', MyView.as_view()),
]
值得注意的是,你的方法返回值和基于函数的视图返回值是相同的,既某种形式的 HttpResponse 。这意味着 http 快捷函数 或 TemplateResponse 对象可以使用基于类里的视图。
虽然基于类的最小视图不需要任何类属性来执行任务,类属性在很多基于类的始终很常见,这里有两种方法来配置或设置类属性。
第一种是子类化标准 Python 方式,并且在子类中覆盖属性和方法。所以如果父类有个像 greeting 这样的属性:
from django.http import HttpResponse
from django.views import View
class GreetingView(View):
greeting = "Good Day"
def get(self, request):
return HttpResponse(self.greeting)
你可以在子类中覆盖它:
class MorningGreetingView(GreetingView):
greeting = "Morning to ya"
另一个选择是在 URLconf 中将配置类属性作为参数来调用 as_view() 。
urlpatterns = [
path('about/', GreetingView.as_view(greeting="G'day")),
]
备注
当你的类为发送给它的每个请求实例化时,通过 as_view() 入口点设置的类属性在导入 URLs 的时候只配置一次
闭包
闭包,又称闭包函数或者闭合函数,其实和前面讲的嵌套函数类似,不同之处在于,闭包中外部函数返回的不是一个具体的值,而是一个函数。一般情况下,返回的函数会赋值给一个变量,这个变量可以在后面被继续执行调用。
例如,计算一个数的 n 次幂,用闭包可以写成下面的代码:
def nth_power(exponent):
"""
闭包函数,其中 exponent 称为自由变量
:param exponent:
:return:
"""
def exponent_of(base):
return base ** exponent
return exponent_of # 返回值是 exponent_of 函数
square = nth_power(2) # 计算一个平方
cube = nth_power(3) # 计算一个立方
print(square(2)) # 计算 2 的平方
print(cube(2)) # 计算 2 的立方
运行结果为:
4
6
在上面程序中,外部函数 nth_power() 的返回值是函数 exponent_of(),而不是一个具体的数值。
需要注意的是,在执行完 square = nth_power(2) 和 cube = nth_power(3) 后,外部函数 nth_power() 的参数 exponent 会和内部函数 exponent_of 一起赋值给 squre 和 cube,这样在之后调用 square(2) 或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义。
lambda表达式(匿名函数)及用法
对于定义一个简单的函数,Python 还提供了另外一种方法,即使用本节介绍的 lambda 表达式。
lambda 表达式,又称匿名函数,常用来表示内部仅包含 1 行表达式的函数。如果一个函数的函数体仅有 1 行表达式,则该函数就可以用 lambda 表达式来代替。
lambda 表达式的语法格式如下:
name = lambda [list] : 表达式
其中,定义 lambda 表达式,必须使用 lambda 关键字;[list] 作为可选参数,等同于定义函数是指定的参数列表;value 为该表达式的名称。
该语法格式转换成普通函数的形式,如下所示:
def name(list):
return 表达式
name(list)
显然,使用普通方法定义此函数,需要 3 行代码,而使用 lambda 表达式仅需 1 行。
举个例子,如果设计一个求 2 个数之和的函数,使用普通函数的方式,定义如下:
def add(x, y):
return x+ y
print(add(3,4))
由于上面程序中,add() 函数内部仅有 1 行表达式,因此该函数可以直接用 lambda 表达式表示:
add = lambda x,y:x+y
print(add(3,4))
# 7
可以这样理解 lambda 表达式,其就是简单函数(函数体仅是单行的表达式)的简写版本。相比函数,lamba 表达式具有以下 2 个优势:
- 对于单行函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁;
- 对于不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高程序执行的性能。
难道PEP不推荐我们使用lambda表达式吗?其实不然。
出现警告的原因是:
因为你把lambda表达式赋给了另一个变量。但lambda表达式本就是一个匿名的函数,PEP8规范并不推荐将lambda表达式赋值给一个变量,再通过变量调用函数这种方式。这种方式不能体现lambda表达式的特色,基本只是复制def的功能,同时这个变量名其实也不是lambda表达式真正的函数名,还显得比def方式更容易混淆。事实上lambda表达式的正确用法应该是在不分配变量的情况下使用,例如使用作为函数的实参等情况。
@classmethod类方法和静态方法@staticmethod
@classmethod类方法
Python 类方法和实例方法相似,它最少也要包含一个参数,只不过类方法中通常将其命名为 cls,Python 会自动将类本身绑定给 cls 参数(注意,绑定的不是类对象)。也就是说,我们在调用类方法时,无需显式为 cls 参数传参。
和 self 一样,cls 参数的命名也不是规定的(可以随意命名),只是 Python 程序员约定俗称的习惯而已。
和实例方法最大的不同在于,类方法需要使用 @classmethod 修饰符进行修饰,例如:
class CLanguage:
#类构造方法,也属于实例方法
def __init__(self):
self.name = "C语言中文网"
self.add = "http://c.biancheng.net"
#下面定义了一个类方法
@classmethod
def info(cls):
print("正在调用类方法",cls)
注意,如果没有 @classmethod,则 Python 解释器会将 fly() 方法认定为实例方法,而不是类方法。
类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)。例如,在上面 CLanguage 类的基础上,在该类外部添加如下代码:
#使用类名直接调用类方法
CLanguage.info()
#使用类对象调用类方法
clang = CLanguage()
clang.info()
运行结果为:
正在调用类方法 <class '__main__.CLanguage'>
正在调用类方法 <class '__main__.CLanguage'>
@staticmethod静态方法
静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。
静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。
静态方法需要使用 @staticmethod 修饰,例如:
class CLanguage:
@staticmethod
def info(name,add):
print(name,add)
静态方法的调用,既可以使用类名,也可以使用类对象,例如:
#使用类名直接调用静态方法
CLanguage.info("C语言中文网","http://c.biancheng.net")
#使用类对象调用静态方法
clang = CLanguage()
clang.info("Python教程","http://c.biancheng.net/python")
在实际编程中,几乎不会用到类方法和静态方法,因为我们完全可以使用函数代替它们实现想要的功能,但在一些特殊的场景中(例如工厂模式中),使用类方法和静态方法也是很不错的选择。
MetaClass元类详解
MetaClass元类,本质也是一个类,但和普通类的用法不同,它可以对类内部的定义(包括类属性和类方法)进行动态的修改。可以这么说,使用元类的主要目的就是为了实现在创建类时,能够动态地改变类中定义的属性或者方法。
举个例子,根据实际场景的需要,我们要为多个类添加一个 name 属性和一个 say() 方法。显然有多种方法可以实现,但其中一种方法就是使用 MetaClass 元类。
如果在创建类时,想用 MetaClass 元类动态地修改内部的属性或者方法,则类的创建过程将变得复杂:先创建 MetaClass 元类,然后用元类去创建类,最后使用该类的实例化对象实现功能。
和前面章节创建的类不同,如果想把一个类设计成 MetaClass 元类,其必须符合以下条件:
- 1、必须显式继承自 type 类;
- 2、类中需要定义并实现 new() 方法,该方法一定要返回该类的一个实例对象,因为在使用元类创建类时,该 new() 方法会自动被执行,用来修改新建的类。
讲了这么多,读者可能对 MetaClass 元类的功能还是比较懵懂。没关系,我们先尝试定义一个 MetaClass 元类:
#定义一个元类
class FirstMetaClass(type):
# cls代表动态修改的类
# name代表动态修改的类名
# bases代表被动态修改的类的所有父类
# attr代表被动态修改的类的所有属性、方法组成的字典
def __new__(cls, name, bases, attrs):
# 动态为该类添加一个name属性
attrs['name'] = "C语言中文网"
attrs['say'] = lambda self: print("调用 say() 实例方法")
return super().__new__(cls,name,bases,attrs)
此程序中,首先可以断定 FirstMetaClass 是一个类。其次,由于该类继承自 type 类,并且内部实现了 new() 方法,因此可以断定 FirstMetaCLass 是一个元类。
可以看到,在这个元类的 __new__()
方法中,手动添加了一个 name 属性和 say() 方法。这意味着,通过 FirstMetaClass 元类创建的类,会额外添加 name 属性和 say() 方法。通过如下代码,可以验证这个结论:
#定义类时,指定元类
class CLanguage(object,metaclass=FirstMetaClass):
pass
clangs = CLanguage()
print(clangs.name)
clangs.say()
可以看到,在创建类时,通过在标注父类的同时指定元类(格式为metaclass=元类名),则当 Python 解释器在创建这该类时,FirstMetaClass 元类中的 new 方法就会被调用,从而实现动态修改类属性或者类方法的目的。
显然,FirstMetaClass 元类的 new() 方法动态地为 Clanguage 类添加了 name 属性和 say() 方法,因此,即便该类在定义时是空类,它也依然有 name 属性和 say() 方法。
多态及用法详解
我们都知道,Python 是弱类型语言,其最明显的特征是在使用变量时,无需为其指定具体的数据类型。这会导致一种情况,即同一变量可能会被先后赋值不同的类对象,例如:
class CLanguage:
def say(self):
print("赋值的是 CLanguage 类的实例对象")
class CPython:
def say(self):
print("赋值的是 CPython 类的实例对象")
a = CLanguage()
a.say()
a = CPython()
a.say()
运行结果为:
赋值的是 CLanguage 类的实例对象
赋值的是 CPython 类的实例对象
可以看到,a 可以被先后赋值为 CLanguage 类和 CPython 类的对象,但这并不是多态。类的多态特性,还要满足以下 2 个前提条件:
- 继承:多态一定是发生在子类和父类之间;
- 重写:子类重写了父类的方法。
下面程序是对上面代码的改写:
class CLanguage:
def say(self):
print("调用的是 Clanguage 类的say方法")
class CPython(CLanguage):
def say(self):
print("调用的是 CPython 类的say方法")
class CLinux(CLanguage):
def say(self):
print("调用的是 CLinux 类的say方法")
a = CLanguage()
a.say()
a = CPython()
a.say()
a = CLinux()
a.say()
可以看到,CPython 和 CLinux 都继承自 CLanguage 类,且各自都重写了父类的 say() 方法。从运行结果可以看出,同一变量 a 在执行同一个 say() 方法时,由于 a 实际表示不同的类实例对象,因此 a.say() 调用的并不是同一个类中的 say() 方法,这就是多态。
但是,仅仅学到这里,读者还无法领略 Python 类使用多态特性的精髓。其实,Python 在多态的基础上,衍生出了一种更灵活的编程机制。
更多请查看:python多态及详解
枚举定义和使用
一些具有特殊含义的类,其实例化对象的个数往往是固定的,比如用一个类表示月份,则该类的实例对象最多有 12 个;再比如用一个类表示季节,则该类的实例化对象最多有 4 个。
针对这种特殊的类,Python 3.4 中新增加了 Enum 枚举类。也就是说,对于这些实例化对象个数固定的类,可以用枚举类来定义。
在使用时通过 from enum import Enum
来引入。开发人员需要自己定义一个继承Enum的类来实现枚举类型对象。python的枚举是使用类来实现的,类属性是枚举名称,属性值对应枚举值。Enum的使用有如下特点:
- 1、枚举类不允许定义相同枚举名称,但不同的枚举名称可以有相同的值,后者相当于前者的别名。
- 2、枚举值不能被修改,枚举值一旦被修改,就会引发AttributeError异常。
- 3、两个不同的枚举类,枚举名称和枚举值即便相同,在比较时也是不相等的。
- 4、枚举类的一个枚举有name(标签)和value(枚举值)两个属性,使用枚举值时,务必通过value获取枚举值。
2. 枚举应用场景
枚举有什么作用呢?当一个变量有几种固定的取值时,通常我们喜欢将它定义为枚举类型,枚举类型用于声明一组命名的常数,使用枚举类型可以增强代买的可读性。
假设有这样一个函数
def print_color(color_code):
if color_code == 1:
print('红色')
elif color_code == 2:
print('蓝色')
elif color_code == 3:
print('黑色')
参数color_code取值有3种,1表示红色,2表示蓝色,3表示黑色。color_code是表示颜色的代码,只有3种取值,这种情形下就适合使用枚举类型来表示,在python没有枚举类型之前,可以使用类来定义枚举类型。
class ColorCode:
RED = 1
BLUE = 2
BLACK = 3
def print_color(color_code):
if color_code == ColorCode.RED:
print('红色')
elif color_code == ColorCode.BLUE:
print('蓝色')
elif color_code == ColorCode.BLACK:
print('黑色')
print_color(1)
函数里不再用color_code和1,2,3这些整数值进行比较,而是与ColorCode的类属性进行比较,代码可阅读性更好,因为只看1,2,3,你无法理解这些数字所代表的含义。虽然使用类可以模拟枚举类型,但这种技术有一个缺点,类属性可以随意修改
ColorCode.RED = 4
枚举类型要求一旦完成定义,就不能再修改,否则使用枚举的地方将由于枚举值的改变出现不可知的问题。
3、Enum使用示例
python3 提供了enum模块,定义类时继承enum.Enum,可以创建一个枚举类型数据,除此以外还可以继承enum.IntEnum,枚举值只能是int。
下面的代码演示如何通过继承Enum定义一个枚举类
import enum
class ColorCode(enum.Enum):
RED = 1
BLUE = 2
BLACK = 3
def print_color(color_code):
if color_code == ColorCode.RED.value:
print('红色')
elif color_code == ColorCode.BLUE.value:
print('蓝色')
elif color_code == ColorCode.BLACK.value:
print('黑色')
print_color(1)
看上去和第2小节里的代码没有什么大的区别,但由于继承了enum.Enum,ColorCode的类属性将无法修改,如果执行
ColorCode.RED = 4
将会引发错误
raise AttributeError('Cannot reassign members.')
AttributeError: Cannot reassign members.
枚举值不能被修改,是使用枚举类型进行编程的最重要的目的之一,假设枚举值可以被修改,那么也就没有必要提供enum这个模块了,我们使用自定义类和类属性就能够替代enum模块。
4、实战
enum/task_status_enum.py
from enum import Enum
class TaskStatusEnum(Enum):
"""
任务状态 0:未启动 1:进行中 2:成功 3:失败
"""
# 未启动
UNSTART = 0
# 进行中
RUNNING = 1
# 成功
SUCCESS = 2
# 失败
FAIL = 3
if __name__ == '__main__':
print(TaskStatusEnum.FAIL.value)
for status in TaskStatusEnum:
print(status)
相关文章:
《Python核心技术与实战》学习笔记
python 装饰器这一篇就够了
python中的*args与**kwargs的含义与作用
基于类的视图
djangoproject源码
Python闭包函数
lambda表达式匿名函数
python多态及详解
酷Python|python枚举(enum)
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)