พื้นฐาน

ฟังก์ชันใน Python: def, return, อาร์กิวเมนต์, *args และ **kwargs, lambda และ annotation

อัปเดต: · Skillaria.Top

ฟังก์ชันใน Python คือบล็อกโค้ดที่มีชื่อ ประกาศด้วยคำว่า def เรียกใช้ด้วยชื่อพร้อมวงเล็บ และส่งผลลัพธ์กลับด้วย return หากไม่มี return ฟังก์ชันจะคืนค่า None อาร์กิวเมนต์ส่งตามลำดับหรือระบุชื่อได้ *args รวมค่าตำแหน่งที่เหลือเป็นทูเพิล **kwargs รวมค่าแบบชื่อเป็นดิกชันนารี และ lambda สร้างฟังก์ชันไม่มีชื่อจากนิพจน์เดียว

ฟังก์ชันใน Python คือชิ้นส่วนของโค้ดที่มีชื่อ เรียกใช้ซ้ำได้หลายครั้งด้วยข้อมูลนำเข้าที่ต่างกัน ประกาศด้วยคำว่า def รับค่าผ่านพารามิเตอร์ และส่งผลลัพธ์กลับด้วย return ฟังก์ชันคือเครื่องมือหลักในการแปลงสคริปต์ยาว ๆ ให้เป็นขั้นตอนที่อ่านเข้าใจและตรวจสอบได้ง่าย

การประกาศฟังก์ชันด้วย def

การประกาศฟังก์ชันเริ่มด้วย def ตามด้วยชื่อ พารามิเตอร์ในวงเล็บ และเครื่องหมายโคลอน เนื้อหาของฟังก์ชันเขียนโดยเยื้อง 4 ช่องว่าง การเยื้องไม่ใช่แค่ความสวยงาม แต่เป็นส่วนหนึ่งของไวยากรณ์

def greet(name):
    """คืนข้อความทักทายสำหรับผู้ใช้"""
    return f"สวัสดี {name}!"

print(greet("อนงค์"))   # สวัสดี อนงค์!

ข้อความในเครื่องหมายคำพูดสามชั้นที่อยู่ใต้บรรทัดประกาศคือ docstring หรือเอกสารของฟังก์ชัน ซึ่ง help(greet) และคำแนะนำของโปรแกรมแก้ไขโค้ดจะแสดงให้เห็น ชื่อฟังก์ชันตามข้อกำหนด PEP 8 ใช้ snake_case เช่น add_expense หรือ get_rate

บรรทัด def greet(name): เรียกว่า signature ของฟังก์ชัน ซึ่งประกอบด้วยชื่อและรายการพารามิเตอร์ (และ annotation หากมี) จาก signature ผู้อ่านรู้วิธีเรียกใช้ฟังก์ชันได้โดยไม่ต้องดูเนื้อหาข้างใน

return ใน Python ทำอะไร

return จบการทำงานของฟังก์ชันและส่งค่ากลับไปให้ผู้ที่เรียกใช้ โค้ดที่อยู่หลัง return ที่ทำงานไปแล้วจะไม่ถูกรัน หากไม่มี return หรือเขียนโดยไม่ระบุค่า ฟังก์ชันจะคืน None

def area(w, h):
    w * h          # คำนวณแล้วแต่ไม่ได้ส่งออก

print(area(3, 4))  # None

def area(w, h):
    return w * h

print(area(3, 4))  # 12

การลืมเขียน return เป็นหนึ่งในข้อผิดพลาดที่มือใหม่พบบ่อยที่สุด ฟังก์ชันทำงานได้ปกติแต่คืน None ทำให้ปัญหาไปโผล่ที่บรรทัดถัดไป ฟังก์ชันยังคืนหลายค่าได้ ซึ่งจริง ๆ แล้วคือทูเพิลหนึ่งตัวที่สามารถแตกค่าได้สะดวก:

def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([4, 1, 9])
print(low, high)   # 1 9

การ return ก่อนกำหนด (early return) ช่วยลดการซ้อนของโค้ด โดยจัดการกรณีพิเศษก่อน แล้วจึงเขียนตรรกะหลัก

def discount(price, is_member):
    if price <= 0:
        return 0
    if not is_member:
        return price
    return price * 0.9

อาร์กิวเมนต์ของฟังก์ชันใน Python: แบบตำแหน่งและแบบชื่อ

พารามิเตอร์คือชื่อที่ประกาศในฟังก์ชัน ส่วนอาร์กิวเมนต์คือค่าที่ส่งเข้าไปตอนเรียกใช้ สามารถส่งอาร์กิวเมนต์ตามลำดับ (แบบตำแหน่ง) หรือระบุชื่อ (แบบชื่อ) การระบุชื่อทำให้การเรียกอ่านเข้าใจได้เอง และลำดับไม่สำคัญอีกต่อไป

def add_expense(title, amount, category="อื่น ๆ"):
    return {"title": title, "amount": amount, "category": category}

add_expense("กาแฟ", 250)                          # แบบตำแหน่ง
add_expense(amount=250, title="กาแฟ")             # แบบชื่อ
add_expense("แท็กซี่", 600, category="เดินทาง")   # ผสมกัน

พารามิเตอร์ที่มีค่าเริ่มต้นไม่จำเป็นต้องส่งค่า กฎคือพารามิเตอร์ที่มีค่าเริ่มต้นต้องอยู่หลังพารามิเตอร์ที่บังคับ และตอนเรียกใช้ อาร์กิวเมนต์แบบตำแหน่งต้องมาก่อนแบบชื่อ มิฉะนั้น Python จะแสดง SyntaxError

ข้อควรระวัง: ค่าเริ่มต้นที่แก้ไขได้

ค่าเริ่มต้นจะถูกคำนวณเพียงครั้งเดียว คือตอนที่บรรทัด def ทำงาน ไม่ใช่ทุกครั้งที่เรียกฟังก์ชัน หากค่าเป็นลิสต์หรือดิกชันนารี มันจะถูกใช้ร่วมกันในทุกการเรียก

def add_item(item, acc=[]):
    acc.append(item)
    return acc

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] — ลิสต์จำค่าจากการเรียกก่อนหน้า

วิธีที่ถูกต้องคือตั้งค่าเริ่มต้นเป็น None แล้วสร้างอ็อบเจกต์ใหม่ภายในฟังก์ชัน:

def add_item(item, acc=None):
    if acc is None:
        acc = []
    acc.append(item)
    return acc

*args และ **kwargs: รับอาร์กิวเมนต์จำนวนเท่าใดก็ได้

เครื่องหมายดอกจันหน้าพารามิเตอร์จะรวมอาร์กิวเมนต์แบบตำแหน่งที่เหลือทั้งหมดเป็นทูเพิล (tuple) และเครื่องหมายดอกจันสองตัวจะรวมอาร์กิวเมนต์แบบชื่อที่เหลือเป็นดิกชันนารี (dict) ชื่อ args และ kwargs เป็นเพียงธรรมเนียม สิ่งที่สำคัญคือเครื่องหมายดอกจัน

def total(*args):
    return sum(args)

print(total(100, 250, 40))   # 390

def make_user(name, **kwargs):
    user = {"name": name}
    user.update(kwargs)
    return user

print(make_user("อนงค์", city="เชียงใหม่", age=30))
# {'name': 'อนงค์', 'city': 'เชียงใหม่', 'age': 30}

เครื่องหมายดอกจันทำงานในทิศทางตรงกันข้ามได้ด้วย เมื่อใช้ตอนเรียกฟังก์ชัน จะแตกลำดับหรือดิกชันนารีออกเป็นอาร์กิวเมนต์:

values = [3, 4]
print(pow(*values))          # 81

opts = {"sep": " | ", "end": "\n"}
print("a", "b", **opts)      # a | b

พารามิเตอร์แบบตำแหน่งอย่างเดียวและแบบชื่ออย่างเดียว

เครื่องหมายทับ / และเครื่องหมายดอกจัน * ในรายการพารามิเตอร์ทำหน้าที่เหมือนด่าน ทุกพารามิเตอร์ก่อน / ต้องส่งตามลำดับเท่านั้น (ไวยากรณ์นี้มีตั้งแต่ Python 3.8, PEP 570) ส่วนทุกพารามิเตอร์หลัง * ต้องส่งโดยระบุชื่อเท่านั้น

def convert(amount, /, *, rate, currency="USD"):
    return round(amount / rate, 2), currency

convert(100, rate=90.5)          # (1.1, 'USD')
convert(amount=100, rate=90.5)   # TypeError: amount ส่งได้เฉพาะแบบตำแหน่ง
convert(100, 90.5)               # TypeError: rate ต้องระบุชื่อเท่านั้น

พารามิเตอร์ที่ต้องระบุชื่อช่วยป้องกันความสับสนในการเรียกแบบ convert(100, 90.5, "EUR") ที่ไม่ชัดว่าค่าแต่ละตัวคืออะไร

ขอบเขตของตัวแปร: กฎ LEGB

Python ค้นหาชื่อตัวแปรตามลำดับ: Local (ภายในฟังก์ชัน) → Enclosing (ในฟังก์ชันที่ล้อมอยู่) → Global (ระดับโมดูล) → Built-in (ชื่อในตัว เช่น len) การอ่านตัวแปรระดับโมดูลภายในฟังก์ชันทำได้ แต่หากต้องการกำหนดค่าใหม่ ต้องประกาศ global

limit = 1000

def check(amount):
    return amount > limit    # อ่านตัวแปร global ได้

def raise_limit():
    global limit
    limit = 2000             # ถ้าไม่มี global จะสร้างตัวแปร local ใหม่

ในทางปฏิบัติ ควรหลีกเลี่ยง global เพราะฟังก์ชันที่เปลี่ยนสถานะภายนอกจะทดสอบยาก วิธีที่ดีกว่าคือรับค่าผ่านอาร์กิวเมนต์และคืนผลลัพธ์กลับมา

ฟังก์ชันซ้อนและ closure

สามารถประกาศฟังก์ชันไว้ในฟังก์ชันอื่นได้ ฟังก์ชันซ้อนจะมองเห็นตัวแปรของฟังก์ชันภายนอกแม้ฟังก์ชันภายนอกจะทำงานจบแล้ว สิ่งนี้เรียกว่า closure และหากต้องการแก้ไขตัวแปรเหล่านั้น ให้ใช้ nonlocal

def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c(), c(), c())   # 1 2 3

Closure เป็นพื้นฐานของ decorator และการสร้างฟังก์ชันจากโรงงาน (function factory)

ฟังก์ชันคือออบเจกต์

ใน Python ฟังก์ชันเป็นออบเจกต์ธรรมดา สามารถเก็บไว้ในตัวแปร ส่งเป็นอาร์กิวเมนต์ให้ฟังก์ชันอื่น คืนค่าจากฟังก์ชัน หรือเก็บไว้ในดิกชันนารีได้ ฟังก์ชันมีแอตทริบิวต์ เช่น __name__ __doc__ __defaults__ และ __annotations__

def double(x):
    return x * 2

f = double
print(f(5), f.__name__)                # 10 double
print(list(map(double, [1, 2, 3])))   # [2, 4, 6]

lambda ใน Python: ฟังก์ชันไม่มีชื่อ

ฟังก์ชัน lambda คือฟังก์ชันสั้น ๆ ที่ไม่มีชื่อ และเนื้อหาเป็นนิพจน์เดียว ผลของนิพจน์จะถูกคืนโดยอัตโนมัติ ไม่ต้องเขียน return

square = lambda x: x ** 2
print(square(4))   # 16

expenses = [("กาแฟ", 250), ("แท็กซี่", 600), ("อาหารกลางวัน", 450)]
expenses.sort(key=lambda e: e[1], reverse=True)
print(expenses)  # [('แท็กซี่', 600), ('อาหารกลางวัน', 450), ('กาแฟ', 250)]

print(list(filter(lambda n: n % 2 == 0, range(10))))  # [0, 2, 4, 6, 8]

lambda เหมาะเป็นอาร์กิวเมนต์สั้น ๆ ของ sorted max map และ filter แต่ PEP 8 ไม่แนะนำให้นำ lambda ไปกำหนดให้ตัวแปร เช่น square ด้านบน หากต้องการชื่อให้ใช้ def เพราะจะได้ __name__ ที่มีความหมายในข้อความ traceback และมีที่สำหรับเขียน docstring

deflambda
ชื่อมีไม่มี (<lambda>)
เนื้อหาคำสั่งกี่บรรทัดก็ได้นิพจน์เดียว
returnเขียนเองอัตโนมัติ
Docstring และ annotationได้ไม่ได้
เหมาะกับทุกกรณีkey หรือ callback สั้น ๆ

Annotation ของชนิดข้อมูล: คำแนะนำ ไม่ใช่การตรวจสอบ

Annotation บอกชนิดข้อมูลที่คาดหวังของพารามิเตอร์และผลลัพธ์ Python ไม่ได้ตรวจสิ่งเหล่านี้ขณะรันโปรแกรม ฟังก์ชันที่ระบุ x: int ยังรับสตริงได้ การตรวจทำโดยเครื่องมือภายนอก เช่น mypy, pyright หรือ ty และโปรแกรมแก้ไขโค้ดที่ขีดเส้นใต้ข้อผิดพลาดก่อนรัน

def add_expense(title: str, amount: float, category: str = "อื่น ๆ") -> dict[str, object]:
    return {"title": title, "amount": amount, "category": category}

def area(w: float, h: float) -> float:
    w * h   # mypy: Missing return statement

รันการตรวจได้โดยไม่ต้องติดตั้งด้วยคำสั่ง uvx mypy . ตัวอย่างข้างต้นแสดงประโยชน์ของ annotation เมื่อลืม return ในฟังก์ชันที่ระบุ -> float เครื่องมือจะจับได้ทันที

ตั้งแต่ Python 3.12 (PEP 695) ฟังก์ชันทั่วไป (generic) เขียนได้โดยไม่ต้องใช้ TypeVar พารามิเตอร์ชนิดข้อมูลวางไว้ในวงเล็บเหลี่ยม และนามแฝงของชนิดข้อมูลประกาศด้วยคำว่า type

def first[T](xs: list[T]) -> T:
    return xs[0]

type Money = int | float

ใน Python 3.14 (PEP 649) annotation กลายเป็นแบบ lazy คือคำนวณเมื่อมีการเข้าถึงครั้งแรก ดังนั้นอ้างอิงคลาสที่ประกาศไว้ด้านล่างได้โดยไม่ต้องใส่เครื่องหมายคำพูด และไม่จำเป็นต้องใช้ from __future__ import annotations อีกต่อไป

"ไม่ได้ส่งค่า" หรือ "ส่ง None": sentinel ใน 3.15

การใช้ None เป็นค่าเริ่มต้นทำให้แยกไม่ออกระหว่าง "ไม่ได้ส่งอาร์กิวเมนต์มา" กับ "ตั้งใจส่ง None" มาหลายปีที่แก้ปัญหานี้ด้วยการสร้างตัวทำเครื่องหมายเอง:

_MISSING = object()

def get_rate(currency, default=_MISSING):
    rates = {"USD": 90.5}
    if currency in rates:
        return rates[currency]
    if default is _MISSING:
        raise KeyError(currency)
    return default

ใน Python 3.15 มีฟังก์ชันในตัวชื่อ sentinel (PEP 661): MISSING = sentinel("MISSING") สร้างตัวทำเครื่องหมายที่ไม่ซ้ำกัน มี repr ที่อ่านเข้าใจได้ คงความเป็นตัวเองเมื่อคัดลอก serialize ผ่าน pickle ได้ และนำไปใช้ใน annotation เช่น float | MISSING ได้ ส่วนเวอร์ชันเก่ายังใช้วิธี object() ได้ตามเดิม

ข้อผิดพลาดที่พบบ่อยกับฟังก์ชัน

  • ลืม return ฟังก์ชันจะคืน None
  • ลืมวงเล็บ print(greet) จะแสดงออบเจกต์ของฟังก์ชัน ไม่ใช่ผลของการเรียก
  • ค่าเริ่มต้นที่แก้ไขได้ ทำให้ลิสต์ถูกใช้ร่วมกันทุกการเรียก
  • จำนวนอาร์กิวเมนต์ไม่ถูกต้อง เช่น TypeError: greet() missing 1 required positional argument: 'name'
  • ตั้งชื่อซ้ำกับชื่อในตัว เช่น ประกาศฟังก์ชันชื่อ list หรือ sum จะบดบังฟังก์ชันในตัว

สรุป

ฟังก์ชันใน Python ประกาศด้วย def คืนผลลัพธ์ด้วย return และรับอาร์กิวเมนต์ตามลำดับหรือระบุชื่อ *args และ **kwargs ให้ความยืดหยุ่น ส่วน / และ * ให้ความเข้มงวดในการเรียก lambda ให้ความกระชับสำหรับฟังก์ชันใช้ครั้งเดียว และ annotation ร่วมกับ mypy จับข้อผิดพลาดก่อนรันโปรแกรม ในคอร์สเรื่องนี้ถูกสอนแบบลงมือทำ โดยแบ่งโปรแกรมติดตามค่าใช้จ่ายออกเป็นฟังก์ชันที่มี annotation และตรวจสอบด้วย mypy

คำถามที่พบบ่อย

พารามิเตอร์ต่างจากอาร์กิวเมนต์อย่างไร?

พารามิเตอร์คือชื่อที่ประกาศในฟังก์ชัน ส่วนอาร์กิวเมนต์คือค่าที่ส่งเข้ามาตอนเรียกใช้ ใน def greet(name) คำว่า name คือพารามิเตอร์ และใน greet("อนงค์") สตริง "อนงค์" คืออาร์กิวเมนต์

เรียกฟังก์ชันก่อนประกาศได้หรือไม่?

ในระดับโมดูลไม่ได้ เพราะคำสั่ง def ต้องทำงานก่อนการเรียก มิฉะนั้นจะเกิด NameError แต่หากเรียกจากภายในฟังก์ชันอื่น ก็เรียกฟังก์ชันที่ประกาศไว้ด้านล่างได้ ขอเพียงการเรียกเกิดขึ้นหลังจากการประกาศ

ฟังก์ชันคืนได้กี่ค่า?

ตามหลักแล้วคืนได้หนึ่งค่า แต่ค่านั้นอาจเป็นทูเพิลก็ได้ return a, b คืนทูเพิลสองตัว ซึ่งแตกค่าได้ด้วย x, y = f()

ต้องเขียน annotation ทุกครั้งหรือไม่?

ไม่จำเป็น Python ทำงานได้โดยไม่มี annotation แต่ในโค้ดที่คนอื่นต้องอ่านและดูแล annotation ช่วยให้บำรุงรักษาง่ายขึ้นมาก และทำให้ mypy หรือ pyright จับข้อผิดพลาดได้ก่อนรัน

ใน lambda เขียน if ได้หรือไม่?

ได้เฉพาะนิพจน์เงื่อนไข เช่น lambda x: "คู่" if x % 2 == 0 else "คี่" ส่วนคำสั่ง ลูป และการกำหนดค่าภายใน lambda ไม่ได้

เรียนหัวข้อนี้ผ่านวิดีโอ

หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Python 3.15: от новичка до профи”

ไปที่คอร์ส