本週作業與進度:
1. 研讀The Quick Python Book 3rd Ch9
2. f-string
3. Python: 裝飾器(decorator)
4. 研讀 Advanced R: Function operators



Python functions

Python

First-Class object

函數為 『一級物件(first-class object)』
1. 可指派給變數

def yell(text):
    return text.upper() + '!'

yell('hello')
## 'HELLO!'
bark = yell
bark('hello')

# 
## 'HELLO!'
id(yell)  # 查詢物件yell的内存位址
## 4620874344
id(bark)  # 查詢物件bark的內存位址
## 4620874344
del yell  # 移除掉yell標籤與此函數物件的連結

bark('hello') # 但bark標籤與此函數物件連結仍存在
## 'HELLO!'
bark.__name__
## 'yell'
(lambda x: x**2).__name__ ## 補充:lambda函數的name為lambda
## '<lambda>'
  1. 可存在資料結構中
funcs = [bark, str.lower, str.capitalize]
funcs
## [<function yell at 0x1136cf268>, <method 'lower' of 'str' objects>, <method 'capitalize' of 'str' objects>]
funcs[1]('NCCU')
## 'nccu'
  1. 可作為參數傳給其他函數
def greet(func):
    greeting = func('Hi, I am a Python Program')
    print(greeting)
def wisper(text):
    return text.lower() + '...'
greet(bark)
## HI, I AM A PYTHON PROGRAM!
greet(funcs[2])
## Hi, i am a python program
greet(str.swapcase)
## hI, i AM A pYTHON pROGRAM
greet(wisper)
## hi, i am a python program...
list(map(bark, ['hello', 'hey', 'hi']))
## ['HELLO!', 'HEY!', 'HI!']
  1. 可作為函數的回傳值
  • Nested Function :
    與R一樣,可在函數內定義其他函數:
def speak(text):
    def Whisper(t):
        return t.lower() + '...'
    return Whisper(text)

speak('Hello World')
## 'hello world...'
# Whisper('Yo')
# NameError: name 'Whisper' is not defined
def get_speak_func(volume):
    def Whisper(text):
        return text.lower() + '...'
    def Yell(text):
        return text.upper() + '!'
    if volume > 0.5:
        return Yell
    else:
        return Whisper
get_speak_func(0.3)
## <function get_speak_func.<locals>.Whisper at 0x11368d048>
get_speak_func(0.7)('Hello')
## 'HELLO!'
  • 內部函數可記住父函數的狀態(環境)-closure(閉包),又可稱為 function factory
  • “An object is data with functions. A closure is a function with data.” — John D. Cook
def get_speak_func(volume, text):
    def Whisper():
        return text.lower() + '...'
    def Yell():
        return text.upper() + '!'
    if volume > 0.5:
        return Yell
    else:
        return Whisper
        
func = get_speak_func(volume=0.7, text='Hello, World')
func()  # 記住了父函數參數text值
## 'HELLO, WORLD!'
def power(exponent):
    def f(x):
        return x ** exponent
    return f

square = power(2)
cubic = power(3)
x = square(3)
x
## 9
y = cubic(3)
y
## 27
callable(square)
## True
callable(x)
## False
def counter():
    i = 0
    def count():
        nonlocal i
        i = i + 1
        return i
    return count

counter_one = counter()
counter_one()
## 1
counter_one()
## 2
counter_two = counter()
counter_two()
## 1
counter_two()
## 2
counter_two()
## 3
  • R版本:
new_counter <- function() {
  i <- 0
  function() {
    i <<- i + 1
    i
  }
}
counter_1 = new_counter()
counter_1()
## [1] 1
counter_1()
## [1] 2

Lambda function

  • Python的關鍵字 lambda 可針對小型函數定義 匿名函數(anonymous function)
  • 只能寫一條運算式
  • 不需要return,自動將運算式當作回傳值
  • 常用於須快速用上函數物件的情境,如當作『函數參數』使用
  • 但不要濫用lambda函數,會降低程式的閱讀性
def add1(x, y):
    return x + y
    
add2 = lambda x, y: x + y

add1(1, 2)
## 3
add2(1, 2)
## 3
(lambda x, y: x + y)('A', 'B')
## 'AB'
  • 作為函數參數使用:
l = ['d', 'b', 'c', 'a']
tuples_list = list(enumerate(l))
tuples_list
## [(0, 'd'), (1, 'b'), (2, 'c'), (3, 'a')]
sorted(tuples_list)
## [(0, 'd'), (1, 'b'), (2, 'c'), (3, 'a')]
sorted(tuples_list, key = lambda x: x[1])
## [(3, 'a'), (1, 'b'), (2, 'c'), (0, 'd')]
sorted(range(-5, 6), key = lambda x: x ** 2)
## [0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]
  • closure示範:
def make_adder(n):
    return lambda x: x + n
plus_3 = make_adder(3)
plus_3(5)
## 8
action = (lambda x: (lambda y: x + y))
act = action("99")
act("3")
## '993'
list(filter(lambda x: x % 2 == 0, range(16)))  # 不好的寫法
## [0, 2, 4, 6, 8, 10, 12, 14]
[x for x in range(16) if x % 2 == 0]           # 較好的寫法
## [0, 2, 4, 6, 8, 10, 12, 14]

Funcationals

其他內建高階函數

map()

filter()

reduce()

  • functools模組之reduce函數
from functools import reduce
reduce(lambda x, y: x + y, ['a', 'b', 'c', ])
## 'abc'
def my_reduce(function, sequence): 
    tally = sequence[0]
    for next in sequence[1:]:
        tally = function(tally, next)
    return tally

my_reduce(lambda x, y: x * y, [2, 4, 6, 8])
## 384

Decorator(裝飾器)

  • 在不永久性修改物件本身,讓你修改可呼叫物件(如function、method)的行為
  • 『裝飾器』的功能為『修飾』或『包裝』另一支函數
  • 『裝飾器』是一個可呼叫物件,其輸入參數與回傳值也可以為可呼叫物件
  • 事實上,『裝飾器』可以用來裝飾任何物件:包含函數、方法、產生器(generator)與類別(class)

一般來說用途可有以下情形:

  • logging
  • 監測函數運算結果或衡量執行時間
  • 改變行為
  • 驗證參數
def null_decorator(f):
    return f
def greet():
    return 'Hello'
greet = null_decorator(greet)
greet()
## 'Hello'
  • 可用 @ 語法糖 (syntactic sugar)來修飾函數:
  • 使用 @ 語法糖時,則函數在被定義時 立即被修飾
@null_decorator
def greet2():
    return 'Hello'
    
greet2()
## 'Hello'
def decorate(func):
    def wrapper_func(*args):
        print('原函數執行前')
        func(*args)
        print('原函數已執行')
    return wrapper_func

@decorate
def myfun(parameter):
    print(parameter)

myfun('Hello')
## 原函數執行前
## Hello
## 原函數已執行
def uppercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.upper()
        return modified_result
    return wrapper
    
@uppercase
def greet3():
    return 'Hello'
greet3
## <function uppercase.<locals>.wrapper at 0x1136a2a60>
greet3()
## 'HELLO'
  • 裝飾具有參數的函數: 使用 *args 與 **kwargs
def proxy(func):
    def wrapper(*args, **kwargs):      # 打包用
        return func(*args, **kwargs)   # 解包用
def trace(func):
    def wrapper(*args, **kwargs):
        # 使用f-string
        print(f'追蹤: 呼叫函數 {func.__name__}, 參數為 {args}, {kwargs}')
        original_result = func(*args, **kwargs)
        print(f'追蹤: 函數 {func.__name__}, 傳回 {original_result!r}')
        return original_result
    return wrapper


@trace
def say(name, line):
    return f'{name}: {line}'

say('同學', '早安')
## 追蹤: 呼叫函數 say, 參數為 ('同學', '早安'), {}
## 追蹤: 函數 say, 傳回 '同學: 早安'
## '同學: 早安'
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  # 紀錄開始時間
        value = func(*args, **kwargs)
        end = time.perf_counter()    # 紀錄結束時間
        run_time = end - start
        print(f'函數 {func.__name__} 執行時間為 {run_time} 秒')
        return value
    return wrapper

@timer
def waste_some_time(n):
    for _ in range(n):
        sum([i for i in range(10000)])

waste_some_time(1000)
## 函數 waste_some_time 執行時間為 0.3759743550000001 秒
import functools

@timer
def myfun(n):
    functools.reduce(lambda x, y: x + y, n)
myfun([1, 2, 3, 4, 5, ])
## 函數 myfun 執行時間為 5.021999999854643e-06 秒
  • 可支援『多重裝飾器』:套用順序為 『從下而上』
  • 但套用裝飾器層級太多,則會影響程式效能
def emphasis_1(func):
    def wrapper():
        return '( ' + func() + ' )'
    return wrapper

def emphasis_2(func):
    def wrapper():
        return '{ ' + func() + ' }'
    return wrapper

@emphasis_2
@emphasis_1
def greet():
    return 'Hello!'
    
greet()
## '{ ( Hello! ) }'
  • 使用functools.wraps 讓裝飾器『正名』與除錯
def greet():
    '''說明:傳回友善的問候'''
    return '哈囉~'
decorated_greet = uppercase(greet)
greet.__name__   # 取得原始函數的名稱
## 'greet'
greet.__doc__    # 取得原始函數的文件字串
## '說明:傳回友善的問候'
decorated_greet.__name__  # 取得修飾函數的名稱(被換掉)
## 'wrapper'
decorated_greet.__doc__   # # 取得修飾函數的文件字串(被換掉為None)
import functools

def uppercase(func):
    @functools.wraps(func)
    def wrapper():
        return func().upper()
    return wrapper
    
@uppercase
def greet():
    '''說明:傳回友善的問候'''
    return '哈囉~'
    
greet.__name__
## 'greet'
greet.__doc__
## '說明:傳回友善的問候'
  • 裝飾器帶有可輸入參數,用以調整裝飾器的行為
  • 利用 Closure 的觀念,用另一支函數包住裝飾器
    EX: 依據參數n把修飾後的字串重複印出n次
import functools

def uppercase_repeat(n):
    def uppercase(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            result = result.upper()
            for _ in range(n):
                print(result)
            return result
        return wrapper
    return uppercase

@uppercase_repeat(5)
def say(text):
    return text

say('Good morning, Bob!')
## GOOD MORNING, BOB!
## GOOD MORNING, BOB!
## GOOD MORNING, BOB!
## GOOD MORNING, BOB!
## GOOD MORNING, BOB!
## 'GOOD MORNING, BOB!'