ข้ามไปยังเนื้อหา

Variables & Types

ใน Python คุณกำหนดค่าให้กับชื่อตัวแปรได้เลย — ไม่ต้องประกาศชนิดข้อมูล ชนิดของตัวแปรถูกกำหนดโดยออบเจกต์ที่อ้างถึงในขณะนั้น ไม่ใช่โดยการประกาศ

x = 10 # x references an int
x = "hello" # x now references a str — perfectly valid
x = [1, 2, 3] # x now references a list

นี่เรียกว่า dynamic typing ชนิดข้อมูลติดอยู่กับออบเจกต์ ไม่ใช่กับชื่อตัวแปร

ชื่อตัวแปรใน Python ไม่ใช่กล่องที่เก็บค่า — แต่เป็น ป้ายชื่อที่ผูกไว้กับ object = ไม่เคยคัดลอกค่า มันแค่ทำให้ชื่อไปชี้ที่ object ที่มีอยู่แล้ว (หรือเพิ่งสร้างขึ้น) ถ้าชื่อสองชื่อชี้ไปที่ object ที่ mutable ตัวเดียวกัน การแก้ไขผ่านชื่อหนึ่งจะเห็นผลผ่านอีกชื่อหนึ่งด้วย:

a = [1, 2]
b = a # b is bound to the SAME object as a, not a copy
b.append(3)
print(a) # [1, 2, 3] -- mutating through b also changes what a sees
print(a is b) # True -- same object, two names
c = a.copy() # a new, independent object with equal contents
c.append(99)
print(a) # [1, 2, 3] -- unaffected by mutating c
print(a is c) # False

id(obj) คืนตัวเลขที่ไม่ซ้ำกันสำหรับ object ตลอดอายุของมัน (ใน CPython คือตำแหน่งในหน่วยความจำ) ชื่อสองชื่ออ้างถึง object เดียวกันก็ต่อเมื่อ id(a) == id(b) — ซึ่งเป็นสิ่งเดียวกับที่ a is b ตรวจให้คุณอยู่แล้ว นี่คือรูปแบบ bug ที่พบบ่อยที่สุดในโค้ดที่ “ทดสอบแล้วผ่าน”: function รับ list เข้ามาแล้วแก้ไขในที่ (in place) โดยคิดว่าตัวเองมี copy ส่วนตัว แต่ list ของผู้เรียกก็เปลี่ยนไปด้วย

== ถามว่า “object สองตัวนี้มีค่าเท่ากันไหม” (เรียก __eq__) is ถามว่า “ชื่อสองชื่อนี้ผูกกับ object ตัวเดียวกันจริง ๆ ไหม” (เทียบ id()) สำหรับจำนวนเต็มขนาดเล็กและ literal บางแบบ รายละเอียดการ implement ของ CPython ทำให้ is “ดูเหมือน” ตรวจค่าได้ด้วย — จนกว่ามันจะไม่ทำงานแบบนั้นอีกต่อไป:

small1 = 100
small2 = 100
print(small1 is small2) # True -- CPython caches small ints -5..256
big1 = 257
big2 = 257
print(big1 is big2) # True -- literal 257 folded to one constant in this file
big3 = int("257")
big4 = int("257")
print(big3 is big4) # False -- built at runtime, two separate objects

big1 is big2 พิมพ์ True ในตัวอย่างนี้เพราะ literal 257 ทั้งสองตัวอยู่ในโค้ดที่ compile รวมกัน แล้ว compiler เอา constant มารวมเป็นตัวเดียว (constant folding) — นี่เป็นรายละเอียดของไฟล์นี้ ไม่ใช่ข้อรับประกันของภาษา ทันทีที่ค่าเดียวกันเกิดจากการคำนวณ (int("257"), ผลบวก, ค่าที่อ่านจากไฟล์) มันจะเป็น object ใหม่ และ is จะคืน False อย่าใช้ is เทียบค่าเด็ดขาด — ใช้ == เสมอ เก็บ is ไว้สำหรับการตรวจ identity ที่ตั้งใจจริง ๆ เช่น x is None, x is True, x is some_specific_object

Python มีชนิดข้อมูล scalar สี่ชนิดที่คุณจะใช้ในทุกโปรแกรม:

ชนิดตัวอย่างค่าหมายเหตุ
int42, -7, 0ความแม่นยำไม่จำกัด — ไม่มี overflow
float3.14, -0.5ทศนิยม IEEE 754 double precision
str"hello", 'world'ลำดับ Unicode ที่เปลี่ยนแปลงไม่ได้
boolTrue, Falsesubclass ของ int; True == 1, False == 0

ยังมี None — singleton ที่แทนการขาดค่า None มีชนิดข้อมูลของตัวเอง คือ NoneType และมี None เพียงออบเจกต์เดียวในกระบวนการ Python ทั้งหมด ทดสอบด้วย is None เสมอ ไม่ใช่ == None

การที่ object เปลี่ยนแปลงตัวเอง “ในที่” (in place) ได้หลังสร้างขึ้นแล้ว โดยไม่กลายเป็น object อื่น เรียกว่า mutability เรื่องนี้ไม่เกี่ยวกับชื่อตัวแปรเลย — มันเป็นคุณสมบัติของ object เอง:

ชนิดMutable?หมายเหตุ
int, float, bool, complexไม่”การเปลี่ยนแปลง” ใด ๆ สร้าง object ใหม่เสมอ
strไม่ทุก method ของ string คืน string ใหม่
tupleไม่slot ของ tuple แก้ไม่ได้ — แต่ element ที่เป็น mutable เอง (list ที่อยู่ใน tuple) ยังเปลี่ยนได้
frozensetไม่คู่แฝดที่ immutable ของ set
listใช่.append, .sort, slice assignment ล้วนแก้ไขในที่
dictใช่เพิ่ม แก้ไข ลบ key/value ได้
setใช่เพิ่มหรือลบ element ได้

Aliasing (สองชื่อ object เดียว) จะสร้างปัญหาเฉพาะกับชนิดที่ mutable เท่านั้น Aliasing int หรือ str ไม่มีอันตราย เพราะไม่มี operation ไหนแก้ string หรือ int ในที่ได้ ทุกชื่อที่ผูกกับมันจะเห็นค่าเดิมที่มันถูกสร้างมาเสมอ

type() คืนชนิดที่แน่นอนของออบเจกต์ isinstance() ทดสอบว่าออบเจกต์เป็น instance ของชนิดนั้นหรือ subclass ของตัวเอง

age = 30
print(type(age)) # <class 'int'>
print(isinstance(age, int)) # True
print(isinstance(True, int)) # True — bool is a subclass of int

Python ไม่แปลงชนิดข้อมูลโดยอัตโนมัติ คุณต้องแปลงอย่างชัดเจนโดยใช้ชื่อชนิดข้อมูลเป็นฟังก์ชัน:

n = int("42") # str -> int
f = float("3.14") # str -> float
s = str(100) # int -> str
b = bool(0) # int -> bool -> False

ถ้าแปลงไม่ได้ Python จะ raise ValueError:

int("hello") # ValueError: invalid literal for int()

ออบเจกต์ Python ทุกชิ้นมีการตีความเป็น boolean ที่ใช้ใน if และ while ค่าต่อไปนี้เป็น falsy — ค่าอื่นๆ ทั้งหมดเป็น truthy:

  • False, None
  • ศูนย์: 0, 0.0, 0j
  • container ว่างเปล่า: "", [], {}, (), set()
name = ""
if name:
print("has name")
else:
print("name is empty") # this branch runs

รายการนี้คือกับดัก: 0, "", และ [] เป็น ค่าที่ถูกต้องและมีอยู่จริง ที่บังเอิญเป็น falsy — มันไม่ใช่สิ่งเดียวกับ “ไม่ได้ให้ค่ามา” if x: แยกไม่ออกระหว่าง “ผู้เรียกส่ง 0 มา” กับ “ผู้เรียกไม่ได้ส่งอะไรมาเลย” เมื่อไหร่ที่ 0, "", หรือ [] เป็น input ที่มีความหมายจริง ต้องตรวจ identity กับ None แทน truthiness:

def set_volume(vol=None):
if vol: # BUG: 0 (mute) is falsy
return vol
return 50
print(set_volume(0)) # 50 -- wrong: mute (0) silently became the default
def set_volume_fixed(vol=None):
if vol is not None: # checks "was something given", not "is it truthy"
return vol
return 50
print(set_volume_fixed(0)) # 0 -- correct

float คือ IEEE 754 double precision — บิตแบบไบนารีจำนวนคงที่ ซึ่งแทนเศษส่วนทศนิยมส่วนใหญ่ไม่ได้แม่นยำ นี่ไม่ใช่ bug ของ Python — ทุกภาษาที่ใช้ binary float มีปัญหานี้เหมือนกัน

import math
from decimal import Decimal
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
print(math.isclose(0.1 + 0.2, 0.3)) # True
print(Decimal("0.1") + Decimal("0.2")) # 0.3

อย่าเทียบ float ด้วย == เว้นแต่คุณสร้างทั้งสองฝั่งจาก literal หรือการคำนวณเดียวกันเป๊ะ ใช้ math.isclose(a, b) สำหรับการเทียบทั่วไป หรือเปลี่ยนไปใช้ decimal.Decimal (สร้างจาก string ไม่ใช่ float) เมื่อต้องการเลขทศนิยมที่แม่นยำ — เงินคือตัวอย่างคลาสสิก

list, str, dict, type, id, input, sum และ builtin อื่น ๆ ล้วนเป็นแค่ชื่อใน built-in scope — ไม่มีอะไรห้ามคุณ rebind มัน:

items = [1, 2, 3]
list = items # shadows the builtin `list` type in this scope
print(list([4, 5])) # TypeError: 'list' object is not callable

เมื่อ list ถูก rebind แล้ว ชื่อนี้จะไม่อ้างถึง builtin type อีกต่อไปในส่วนที่เหลือของ scope นั้น — การเรียก list(...) จะพยายามเรียก object ที่เก็บอยู่ใน list (list ธรรมดา ไม่ใช่ callable) แทนการสร้าง list ใหม่ ทางแก้ที่แท้จริงคือไม่ตั้งชื่อ variable ซ้ำกับ builtin ตั้งแต่แรก del list ก็ลบ shadow ออกได้เหมือนกัน แต่การเปลี่ยนชื่อตัวแปรคือทางแก้ที่ถูกต้อง

True และ False ไม่ใช่ primitive type แยกต่างหาก — bool เป็น subclass ของ int และ True/False ทำตัวเหมือน 1/0 ในบริบทตัวเลขทุกครั้ง:

print(True + True) # 2
print(isinstance(True, int)) # True
print(True == 1) # True

นี่คือเหตุผลที่ sum([True, True, False]) เป็นวิธีที่ใช้ได้จริง (แม้จะแปลกใจครั้งแรก) ในการนับว่ามีกี่ item ใน list ของ boolean ที่เป็น True — มันได้ผลลัพธ์ 2

age: int = 30
price: float = 9.99
greeting: str = "Hello"
active: bool = True
nothing = None
print(type(age))
print(type(price))
print(type(greeting))
print(type(active))
print(type(nothing))
x = int("42")
y = float("3.14")
z = str(100)
print(x, y, z)
falsy_values = [0, "", None, False, [], 0.0]
for v in falsy_values:
print(f"bool({v!r}) = {bool(v)}")

Playground ที่สองด้านล่างเป็นทัวร์ครบวงจรของทุกอย่างในหน้านี้ที่พังได้แบบเงียบ ๆ: aliasing, is เทียบกับ ==, truthiness, ความแม่นยำของ float, shadowing และเลขคณิตของ bool

import math
from decimal import Decimal
# 1. Names bind to objects; assignment never copies.
a = [1, 2]
b = a # b is bound to the SAME list object, not a copy
b.append(3)
print("a:", a)
print("a is b:", a is b)
c = a.copy() # an independent object with an equal value
c.append(99)
print("a after c mutated:", a)
print("a == c:", a == c, "| a is c:", a is c)
# 2. is vs ==: small-int cache and same-file constant folding make "is"
# LOOK like a value comparison... until the value is built at runtime.
small1 = 100
small2 = 100
print("100 is 100 (cached -5..256):", small1 is small2)
big1 = 257
big2 = 257
print("257 is 257 (literals, same file):", big1 is big2)
big3 = int("257")
big4 = int("257")
print("int('257') is int('257'):", big3 is big4)
# 3. Truthiness: 0 / "" / [] are valid values, not "missing".
def set_volume(vol=None):
if vol: # BUG: a real 0 (mute) is falsy, so it falls through to the default
return vol
return 50 # default volume
print("set_volume(0), buggy:", set_volume(0))
def set_volume_fixed(vol=None):
if vol is not None:
return vol
return 50
print("set_volume(0), fixed:", set_volume_fixed(0))
# 4. Float precision.
print("0.1 + 0.2 ==", 0.1 + 0.2)
print("0.1 + 0.2 == 0.3:", 0.1 + 0.2 == 0.3)
print("isclose:", math.isclose(0.1 + 0.2, 0.3))
print("Decimal:", Decimal("0.1") + Decimal("0.2"))
# 5. Shadowing a builtin.
items = [1, 2, 3]
list = items # shadows the builtin list type in this scope
print("list (shadowed name):", list)
try:
list([4, 5])
except TypeError as e:
print(f"list([4, 5]) -> TypeError: {e}")
del list # remove the shadow; the builtin is visible again
print("list([4, 5]) after del:", list([4, 5]))
# 6. bool is a subclass of int.
one = 1
print("True + True ==", True + True)
print("isinstance(True, int):", isinstance(True, int))
print("True == 1:", True == 1, "| True is one:", True is one)

ลองนึกว่าชื่อตัวแปรเป็นป้ายที่ผูกด้วยเชือกเข้ากับ object ไม่ใช่กล่องที่เก็บค่า a = [1, 2] ผูกป้าย a เข้ากับ list object หนึ่งตัว b = a ผูกป้ายที่สอง b เข้ากับ object ตัวเดียวกัน — ไม่มี object ใหม่เกิดขึ้น b.append(3) เขียนเข้าไปใน object นั้น ป้ายทั้งสองยังเห็นการเปลี่ยนแปลงเพราะชี้ไปที่สิ่งเดียวกัน c = a.copy() สร้าง object ใหม่ที่มีเนื้อหาเท่ากันแล้วผูก c เข้ากับมัน — จากจุดนั้นไป object ทั้งสองเปลี่ยนแปลงแยกจากกัน

flowchart LR
  subgraph Names
    aL["a"]
    bL["b"]
    cL["c"]
  end
  obj1(["list object #1: [1, 2, 3]"])
  obj2(["list object #2: [1, 2, 99]"])
  aL --> obj1
  bL --> obj1
  cL --> obj2
a และ b ผูกกับ list object ตัวเดียวกัน ส่วน c ผูกกับ copy ที่แยกออกมา
สิ่งที่ได้ประโยชน์ต้นทุน
Dynamic typingไม่ต้องประกาศชนิดข้อมูล เขียน prototype ได้เร็ว container ยืดหยุ่นerror เรื่องชนิดข้อมูลโผล่ตอนรันไทม์ ไม่ใช่ก่อนโปรแกรมรัน
Assignment คือการผูก ไม่ใช่การคัดลอกO(1) ไม่ว่า object จะใหญ่แค่ไหน — ส่งข้อมูลก้อนใหญ่ไปมาไม่มีต้นทุนAliasing bug: แก้ไขผ่านชื่อหนึ่งกระทบทุกชื่อที่ผูกกับ object เดียวกัน
การตรวจ identity ด้วย isO(1) ไม่ต้องเรียก __eq__ดูเหมือนใช้ได้กับ small int และ literal ในไฟล์เดียวกัน แล้วพังเงียบ ๆ กับค่าที่ใหญ่กว่าหรือสร้างตอนรันไทม์
การแปลงเป็น truthy/falsy ใน ifเขียน condition สั้นกระชับ (if items:)ค่าที่ถูกต้อง (0, "", []) ตกไปอยู่ branch เดียวกับ “ไม่มีค่า”
int แม่นยำไม่จำกัดไม่มี integer overflow แบบเงียบ ๆ เลยfloat ยังมีความแม่นยำแค่ 53 บิต — 0.1 + 0.2 != 0.3
  • “= คัดลอกค่า เหมือนภาษาอื่น ๆ” ไม่จริง — = ผูกชื่อเข้ากับ object เท่านั้น สำหรับ object ที่ mutable ชื่อสองชื่ออาจจบลงที่การอ้างถึง object เดียวกัน
  • “is กับ == ใช้แทนกันได้กับตัวเลขและ string” มันเห็นตรงกันโดยบังเอิญจาก implementation (small-int cache, constant folding) จนกว่าค่าจะหลุดช่วงนั้นหรือถูกสร้างตอนรันไทม์
  • “0 มีความหมายเหมือน None” 0, "", และ [] คือค่าที่ถูกต้องที่บังเอิญเป็น falsy ส่วน None คือ sentinel ที่แทน “ไม่มีค่า” จริง ๆ
  • “0.1 + 0.2 == 0.3 เพราะมันคือเลขคณิตพื้นฐาน” binary floating point แทน 0.1 หรือ 0.2 ไม่ได้แม่นยำ ใช้ math.isclose หรือ Decimal เมื่อความแม่นยำสำคัญ
  • “การตั้งชื่อตัวแปรซ้ำกับ builtin อย่าง list ไม่มีอันตรายถ้าไม่ใช้ builtin ตัวนั้นซ้ำใกล้ ๆ” มัน shadow builtin ไปตลอด scope ที่เหลือ — การเรียก list(...) ครั้งใดก็ตามใน scope เดียวกันหลังจากนั้นจะพัง
  • โค้ดเทียบกับ None ด้วย == แทน is หรือเปล่า? if value == None: ใช้ได้โดยบังเอิญ (NoneType.__eq__ เห็นตรงกัน) แต่ is None คือวิธีที่ถูกต้องและ idiomatic
  • ตัวแปรใช้ชื่อซ้ำกับ builtin หรือเปล่า (list, str, type, id, dict, input, sum, …)? ถ้าใช่ ตรวจดูว่า builtin ตัวนั้นถูกเรียกที่ไหนอีกใน scope เดียวกันหรือไม่
  • if value: ต้องรับ 0, "", หรือ [] เป็น input ที่ถูกต้องจริงหรือเปล่า? ถ้าใช่ ควรเป็น if value is not None:
  • โค้ดเทียบค่า int หรือ str ด้วย is แทน == หรือเปล่า? มีแต่การตรวจ identity เท่านั้น (is None, is True, is some_sentinel) ที่ควรใช้ is
  • โค้ดเทียบ float ด้วย == ตรง ๆ แทนที่จะใช้ math.isclose หรือ Decimal หรือเปล่า?
  • function รับ argument ที่เป็น mutable (list หรือ dict) แล้วแก้ไขในที่โดยผู้เรียกไม่ได้คาดหวังแบบนั้นหรือเปล่า?

🐛 หา bug ในโค้ดนี้

def process(data):
list = get_items(data) # shadows the builtin list
if list == None: # should be "is None"
return []
return list
เฉลย

มี bug ซ้อนกันสองจุดใน 4 บรรทัด อย่างแรก list = get_items(data) shadow builtin type list ภายใน function นี้ — ถ้า process ต้องเรียก list(...) ในภายหลัง จะพังด้วย TypeError: 'list' object is not callable (หรือทำงานผิดเงียบ ๆ ถ้า get_items คืนอะไรบางอย่างที่ callable) อย่างที่สอง if list == None: พึ่งพา __eq__ ที่บังเอิญเห็นตรงกับ identity สำหรับ None แต่ไม่ใช่วิธีที่ถูกต้องหรือปลอดภัยเสมอไป รูปแบบที่ถูกคือ if list is None: วิธีแก้: เปลี่ยนชื่อตัวแปร (items = get_items(data)) และใช้ is None

โค้ดนี้พิมพ์อะไร?
a = [1, 2]
b = a
b.append(3)
print(a)
โค้ดนี้พิมพ์อะไร?
def set_volume(vol=None):
    if vol:
        return vol
    return 50

print(set_volume(0))
ทำไม x = 257; y = 257; print(x is y) ถึงพิมพ์ True ในสคริปต์ ในขณะที่ค่าเดียวกันที่สร้างด้วย int("257") ตอนรันไทม์กลับพิมพ์ False สำหรับ is?
ชนิดข้อมูลใดต่อไปนี้เป็น mutable ใน Python?
จะเกิดอะไรขึ้น?
list = [1, 2, 3]
print(list([4, 5]))
True + True มีค่าเป็นอะไร และเพราะอะไร?