[TOC]

1. 函数的定义

python中函数有两种:

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
  • python自带的函数
  • 用户定义函数

返回多个值

原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便

1.1函数的参数

参数 含义 输入
位置参数 def power(x,n) 实际参数
默认参数 def power(x,n=2) 实际+默认参数(需要改变时)
可变参数 def power(*args) 传入任意个参数,内部组装成tuple
关键字参数 def person(name, age, **kw) 传入带参数名的参数,组装成dict
命名关键字参数 def person(name,age,*, city, job) 限制关键字参数的名字(必须传入参数名)
  • 顺序: 必选参数<---默认参数<---可变参数<---命名关键字参数<---关键字参数
# 关键字参数
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)  

person('hao', 20) # name: Michael age: 30 other: {}
person('hao', 20, gener = 'M', job = 'Engineer') # name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}  
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)  

# 命名关键字参数
def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)

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

# Combination
# 可变 + 关键字参数
def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

f1(1, 2, 3, 'a', 'b')   # a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
f1(1, 2, 3, 'a', 'b', x=99) # a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

# 默认参数 + 命名关键字参数 + 关键字参数
def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

f2(1, 2, d=99, ext=None) # a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

2. 面向对象编程

  • 面向过程: 根据业务逻辑从上到下写代码
  • 面向对象: 对数据与函数绑定到一起,进行封装,这样更快速的开发过程,减少代码重复使用

数据封装、继承和多态是面向对象的三大特点

2.1. 类(抽象概念)和对象(具体概念)

玩具模具(类)-》 火车玩具,飞机玩具..(对象)

类的组成结构

  • 类名:狗
  • 类的属性:一组数据(狗的颜色,性别...)
  • 类的方法: 运行进行操作的方法行为(行为,会叫,会咬人...)-> 用函数设计
类的组成 特性 例子 例子
类名 名称
类的属性 一组数据 狗的颜色,性别 身高,年龄
类的方法 运行进行操作的方法行为 行为,会叫,会咬人 跑,打架

 Python语法基础-函数,类以及调试处理 Python

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
  • 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
  • 继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

(1). 定义类

# 定义类
class Dog(object):

  # 定义初始化方法
  def __init__(self,weight,color):
    """
      self: python会把对象的地址自动传给self,不要自己传
      weight, color: 接收外部属性
    """
    # 定义属性
    self.weight = weight
    self.color = color

  # 魔法方法: 当只打印Dog对象的时候,就可以打印这里的东西
  def __str__(self):
    msg = "dog weight" + self.weight + "color" + self.color
    return "哈哈哈"

  def getweight(self):
    return self.weight
  def getcolor(self):
    return self.color
  def setweight(self):
    self.weight = 100
  def setcolor(self):
    self.color = "green"
  
  # 定义方法
  def bark(self):
    """
      self: python解释器默认把调用该class的对象地址给它
    """
    print("666")
    
  def run(self):
    print("777")


 # 创建对象
huskey = Dog(5, 'Black')  # 创建一个哈士奇
keji = Dog(10, 'Green')  

huskey.bark()  # 哈士奇叫
huskey.run()   # 哈士奇跑
huskey.weight = 100  # 哈士奇属性
huskey.color = 'green'

  • self表示自己,表示调用类的对象本身
  • python中类似 __***__的方法,是魔法方法,有特殊用途

(2). 类的数据封装

面向对象编程的一个重要特点就是数据封装,比如在上面例子中,Dog类的每个实例都有weight和color,我们可以直接在Dog类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Dog类本身是关联起来的,我们称之为类的方法:

class Dog(object):

    def __init__(self, weight, color):
        self.weight = weight
        self.color = color

    def print_dog(self):
        print('%s: %s' % (self.weight, self.color))

我们从外部看Dog类,就只需要知道,创建实例需要给出weightcolor,而如何打印,都是在Dog类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。

(3). 访问限制

  • 从前面Dog类的定义来看,外部代码还是可以自由地修改一个实例的weightcolor属性,我们可以设置内部属性不被外部访问:

class Dog(object):
    # 加下划线
    def __init__(self, weight, color):
        self.__weight = weight
        self.__color = color

    def print_dog(self):
        print('%s: %s' % (self.__weight, __self.color))

huskey = Dog(60, 'green')
print(huskey.__name)      # 无法再访问属性,会出错
  • 但是如果外部代码需要获取weightcolor属性呢?-> 添加get_weightget_color方法
class Dog(object):
    ...

    def get_weight(self):
        return self.__weight

    def get_color(self):
        return self.__color
  • 如果又要允许外部代码修改weightcolor怎么办?->添加set_weightset_color方法
class Dog(object):
    ...

    def set_weight(self,weight):
        self.__weight = weight

    def set_color(self):
        self.__color = color
  • 为什么要费这么大周折呢?之前不是可以直接husky.weight=10直接改吗
    因为在方法中,可以对参数做检查,避免传入无效的参数:
class Dog(object):
    ...

    def set_weight(self,weight):
      if 0<=weight<=100:
        self.__weight = weight
      else:
        raise ValueError('Bad weight')

(4). 获取对象信息

# type():判断对象类型
type([1,2,3])
type('abc') == str
type(huskey)

# isinstance(): 一个对象是否属于某种类型  
isinstance(h, husky)
isinstance(h, cat)

# dir() 获取一个对象的所有属性和方法 
dir('ABC')
dir('huskey')

2.2. 继承和多态

继承就是:父类和子类之间的交互关系

(1)为什么要用继承?

最大的好处是子类获得了父类的全部功能

class Animal(object):
    def run(self):
        print('Animal is running...')

# Dog继承父类Animal,并且自动拥有run属性,可以在Dog对象中直接调用
class Dog(Animal):
    pass

# 子类可以自己增加一些方法(eat),同时也可以重写父类的方法(run)---》这就是多态
class Cat(Animal):
    def run(self):
      print('Dog is running...')

    def eat(self):
      print('Eating meat...')

(2)什么是多态?

在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是反过来就不行了

C是dog,C也是animal
D是animal,但你不能说D也是Dog

# 首先创建对象并查看对象类型
a = list() # a是list类型
isinstance(a, list)   # true
b = Animal() # b是Animal类型
isinstance(b, Animal) # true
c = Dog() # c是Dog类型
isinstance(c, Dog)    # true


# C既是Dog也是Animal
isinstance(c, Animal)  # true  

# 反过来就错了
isinstance(b, Dog)      # false

(3)那么为题来了。这样设置成多态有什么好处呢?

  • 任何依赖父类作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

  • 当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思

  • 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定

def run_twice(animal):
    animal.run()

run_twice(Animal()) # 调用animal的run
run_twice(Dog())    # 调用dog的run
run_twice(Cat())    # 调用cat的run

3. 面向对象高级编程

(1). __slots__的使用

动态绑定允许我们在程序运行的过程中动态给class加上功能, 但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加nameage属性。

class中定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

 s = Student() # 创建新的实例
 s.name = 'Michael' # 绑定属性'name'

 s.score = 99 # 绑定属性'score'-->出错

__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的

(2). @property的使用

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改,解决方案:

  1. 通过set_score设置成绩,在通过get_score获取成绩,在set_score中检查参数(已经讲过)
  2. 使用内置的@property装饰器,既可以检查参数,又可以类似属性那样访问类的变量

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

class Student(object):

    # @property将get_Score方法变成属性
    @property
    def score(self):
        return self._score

    # @score.setter将set_score方法变成属性赋值
    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

s = Student()
s.score = 60 # OK,实际转化为s.set_score(60)
s.score # OK,实际转化为s.get_score()
s.score = 9999   # error

(3). 多重继承MixLn的设计

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系

class Animal(object):
    pass

class Bird(Animal):
    pass

class Parrot(Bird):
    pass

# 此时我们需要加入额外的功能fly
# 先定义好fly的类
class Flyable(object):
    def fly(self):
        print('Flying...')

# 同时继承两个类
class Parrot(Bird, Flyable):
    pass

(4). 定制个性化类

1): __str____repr__

让打印的object更漂亮:

  • __str__: 用print打印
  • __repr__: 直接输入对象名
class Student(object):
    # 加下划线
    def __init__(self, name):
        self.__name = name

    def __str__(self):
      msg = 'Student name is' + self.__name
      return msg

    __repr__ = __str__

s = Student()
print(s)    # 打印出 Student name is.....

s     # 效果跟上面的一样

2): __iter__

将类定义成类似list或者tuple那种,可以用于for循环的作用

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __iter__(self):
        return self # 实例本身就是迭代对象,故返回自己

    def __getitem__(self, n):
        if isinstance(n, int): # n是索引
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
        if isinstance(n, slice): # n是切片
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L
        
    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值

for i in Fib():
  print(n)          # 1,1,2,3,5,.......,75025

# 因为__getitem__的作用,可以index某个值
f = Fib()
f[0]   # 1

# 也可以切片
print(f[0:5])   # 1,1,2,3,5

3): __call__

直接在实例本身上调用的一种构造方法

class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)

s = Student('Michael')
s() # My name is Michael.
  • 实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象

4) 枚举类Enum

定义常量

from enum import Enum, unique

# unique保证没有重复值
@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

# 访问枚举类 
print(Weekday.Mon)         # Weekday.Mon
print(Weekday.Mon.value)   # 1
print(Weekday(1)))         # # Weekday.Mon

5) type()动态创建类Class

type()函数既可以返回一个对象的类型,又可以创建出新的类型

type()和Class()的功能是一样的

要创建一个class对象,type()函数依次传入3个参数:

  • class的名称;
  • 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  • class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。
def fn(self, name='world'): # 先定义函数
  print("hello", name)

Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class

4. 错误处理

(4.1).try-catch->有错误后就结束了不执行以后的

try:
    print('try...')
    r = 10 / int('a')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')
print('END')


try...
except: division by string
finally...
END

try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

(4.2).logging->有错误了打印完信息后继续执行

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

# 结果
ERROR:root:division by zero
Traceback (most recent call last):
  File "err_logging.py", line 13, in main
    bar('0')
  File "err_logging.py", line 9, in bar
    return foo(s) * 2
  File "err_logging.py", line 6, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END  # 继续执行了
  • 通过配置,logging还可以把错误记录到日志文件里,方便事后排查

(4.3).自定义抛出raise错误

class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n
foo('0')

# 结果
Traceback (most recent call last):
  File "err_throw.py", line 11, in <module>
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

5. 调试

方法 优点 缺点
print方法 简单使用 重复多
assert 简单使用 重复多
logging 不会抛出错误,可以输入文档 重复多
pdb python内置调试器 重复多
VS Code 强强强

(5.1).print方法(略过)

(5.2).断言assert

凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

# 结果
Traceback (most recent call last):
  ...
AssertionError: n is zero!

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。如果断言失败,assert语句本身就会抛出AssertionError

(5.3).logging

print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:

import logging
logging.basicConfig(level=logging.INFO)

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

# 结果
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

logging可以指定不同信息的级别

  • debug
  • info
  • warning
  • error

(5.4).pdb.set_trace()

我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点

import pdb

s = '0'
n = int(s)
# 暂停并进入pdb调试环境
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)

(5.5). IDE

直接在代码中设置断掉调试

graph TD; A-->B; A-->C; B-->D; C-->D;
扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄