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 表达式可以在用完之后立即释放,提高程序执行的性能。

file
难道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)

为者常成,行者常至