1. 程式人生 > >Python語法基礎-函式和類

Python語法基礎-函式和類

[TOC]


1. 函式的定義


python中函式有兩種:

  • 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. 類(抽象概念)和物件(具體概念)

玩具模具(類)-》 火車玩具,飛機玩具..(物件)

類的組成結構

  • 類名:狗
  • 類的屬性:一組資料(狗的顏色,性別...)
  • 類的方法: 執行進行操作的方法行為(行為,會叫,會咬人...)-> 用函式設計
類的組成 特性 例子 例子
類名 名稱
類的屬性 一組資料 狗的顏色,性別 身高,年齡
類的方法 執行進行操作的方法行為 行為,會叫,會咬人 跑,打架

  • 類是建立例項的模板,而例項則是一個一個具體的物件,各個例項擁有的資料都互相獨立,互不影響;
  • 方法就是與例項繫結的函式,和普通函式不同,方法可以直接訪問例項的資料;
  • 通過在例項上呼叫方法,我們就直接操作了物件內部的資料,但無需知道方法內部的實現細節。
  • 繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。

(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
graph TD; A-->B; A-->C; B-->D; C-->D;