Tutorial

Type Annotation ใน Python ใช้เถอะนะ

By Arnon Puitrakul - 07 มกราคม 2021 - 1 min read min(s)

Type Annotation ใน Python ใช้เถอะนะ

Python เป็นภาษาที่ได้รับความนิยมในการเอามาใช้หลากหลายงานมาก ๆ อันที่น่าจะฮิตที่สุดน่าจะเป็นการเอามาใช้ทำงานจำพวก Data Science ซะเยอะ เรื่องนั้นช่างมันเถอะ ไม่ใช่ประเด็นที่เราจะมาเล่า แต่เรื่องของเรื่องคือ Python นางเป็นภาษาที่มีความยืดหยุ่นในการเขียนเยอะมาก ท่าเยอะประมาณนึงเลย (ไม่เท่า Javascript) หนึ่งในเรื่องนั้นคือ Dynamic Typing ที่บอกเลยว่า พอเอาไปทำงานใหญ่ ๆ หลาย ๆ  Module ต่อกัน จากเรื่องที่ทำในการเขียนโปรแกรมทำได้ง่าย กลายเป็นเรื่องปวดหัวขึ้นมาทันที วันนี้เราจะพามาดูกันว่า เราจะอยู่กับมันได้อย่างไรกัน

Variable Typing System

a = 10
a = 20

ใน Programming Language ต่าง ๆ เวลาเราเรียน เราก็จะเจอกับเรื่องแรก ๆ เลยคือ เรื่องของการประกาศ และ การใช้งานตัวแปรต่าง ๆ ที่แต่ละภาษานางจะมี Syntax ในการประกาศที่ต่างกัน หรือในบางภาษานางไม่มีเลยก็มี เช่น Python และ PHP ที่เวลาเราจะใช้ตัวแปรอะไร อยู่ ๆ เราก็จะบอกโผล่งขึ้นมาเลย ว่าเออ จะใช้ ทำไม !

int a = 10
a = 20

ตรงกันข้ามเลย คือภาษาที่ต้องมีการประกาศ และบอกว่า ตัวแปรนี้จะเป็นข้อมูลประเภทไหน ตัวอย่างเช่น ภาษา C ทั้งหลาย ที่เราจะต้องบอกมันเสมอว่า มันคืออะไร หลังจากเราประกาศ และ ถ้าเรา Assign ข้อมูลที่ไม่ใช่ Type ของมัน มันก็จะ Error ออกมาทันที หรือถ้าใช้ Text Editor ดี ๆ หน่อยพวก Linter ก็จะเจอว่า เราเขียนผิดได้เลย ก่อนที่เราจะรันโปรแกรมเพื่อทดสอบซะอีก

ถ้าลงลึกเข้าไป มันจะมีเรื่องของการทำ Memory Allocation อะไรอีกมากมาย อันนี้เราขอไม่พูดถึงละกัน ลองไปอ่านเพิ่มเติมเอาได้ ที่ Dynamic Typing มันจะมีความยุ่งยากในการทำงานนิดนึง เพราะ Data Type บางอย่างมีขนาดไม่เท่ากัน

Dynamic Typing (Weak Typing) เป็นดาบสองคม

ความดีงามของภาษาสมัยใหม่ที่เราเห็นได้เป็นจำนวนมาก คือ การใช้ Dynamic Typing หรือการที่เราสามารถรับบทนางจิ้งจกเปลี่ยนสีได้ตลอดเวลา บรรทัดนี้เราอยากให้มันเป็น String ก็เป็นได้ อีกบรรทัดนึง เราก็เป็นนางแปลง แปลงให้มันเป็น Integer ก็ได้ ความดีคือ มันง่ายไง ง่ายมาก ๆ ไม่ต้องมานั่งเปิดตัวแปรใหม่ให้เปลือง Memory เล่น ไม่ต้องมานั่งตามเช็ดล้างอะไรมากมาย

แต่การใช้งานพวกนี้ มันก็มีดาบสองคมเหมือนกัน อย่างที่บอกว่า บรรทัดนึง มันเป็นอย่าง อีกบรรทัดเป็นอย่าง ทำให้เวลาเรา Debug หรือกระทั่งตอนเราเขียน เราอาจจะสับสนได้ว่ามันคืออะไรกันแน่ แน่นอนว่า พวกตัวช่วยอย่าง Linter จะเอาอะไรมาจับละ เพราะแต่ละบรรทัดมันยังไม่เหมือนกันเลย รู้ตัวอีกทีก็ตอนที่เรารันโปรแกรมไปแล้ว ก็ต้องมานั่งตามเช็ค ตามแก้กันยกใหญ่

import datetime

def calculate_age (birth_year) :
	return datetime.datetime.now().year - brith_year

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

ปรากฏว่า Function นี้มัน Error ทำให้โปรแกรมต้องหยุดการทำงานไป ทั้ง ๆ ที่เราก็รับปีมาจริง ๆ นะ แต่ปัญหามันไม่ได้อยู่ที่ ปี หรอก แต่มันอยู่ที่ Data Type ต่างหาก ส่วนใหญ่เวลาเรารับค่าจาก User หรืออ่านจากไฟล์ Data Type มักจะเป็น String ซะหมด พอเรายัดเข้าไป มันก็จะพัง เพราะมันลบกับตัวเลขไม่ได้นั่นเอง ถ้าเราไม่รอบคอบ หรือ เราเขียนเยอะ ๆ เข้า มันก็มีเบลอ ลืมเช็คกันบ้างแหละ

อีกตัวอย่าง เราเจอตั้งแต่เด็ก ๆ ตอนเขียนโปรแกรมใหม่ ๆ เลย เราเขียนโปรแกรมบวกเลข เรารับค่าจากช่อง Text Box มา 2 ช่องแล้วให้มันบวกกัน แล้ว ไปแสดงผลในอีกช่อง เราใส่  กับ 1 เข้าไป อันที่ถูกก็ควรจะเป็น 2 แต่ไหงได้ 11 ออกมา ตอนนั้นที่พึ่งหัดเขียนโปรแกรมได้ไม่ถึงอาทิตย์ก็คือ เอ๋อ ไปเลยจ๊ะ

จะเห็นว่าการใช้งานพวก Strong Type หรือพวก Type Annotation ช่วยให้การตรวจสอบความผิดพลาดทำได้ง่ายขึ้น เพราะ Editor มันจะรู้ว่าตัวแปรนี้ควรจะเป็นอะไร และ เช็คเจอได้ ถ้าเราใส่ผิด ก่อนที่เรารันโปรแกรม ลดเวลาที่เราต้องมานั่งไล่เมื่อเราใส่ผิดได้เยอะมาก ๆ

Function Annotation & Type Hint

หลังจากขายตรงว่าทำไมเราต้องใช้กันแล้ว เราลองมาดูกันว่า ใน Python เราจะทำเรื่องพวกนี้ได้ยังไง เริ่มจากอันที่น่าจะง่ายที่สุดก่อนคือการทำ Type Hint แปลตรง ๆ คือ ใบ้ประเภท เหรอ... ฮ่า ๆ แต่ตรง ๆ เลยคือ บอกใบ้ลงไปเป็นเหมือน Metadata ว่าตัวแปรนี้ควรจะเป็นอะไร

a: int = 10
a = 'Hello'

จากตัวอย่างด้านบน เราบอกว่า เราจะให้ a เป็นตัวแปรที่เก็บ Integer และ กำหนดค่าให้เป็น 10 ไปเลย แต่พอมาบรรทัดที่ 2 เราบอกว่าให้มันเป็นคำว่า Hello ซึ่งเป็น Integer ถามว่าทำได้มั้ย คำตอบคือ ได้ โปรแกรมจะรันได้ ไม่มีปัญหาอะไร อย่างที่บอกมันเป็นแค่ Metadata ไม่ได้มีผลต่อการทำงานของโปรแกรมแต่อย่างใด แต่มันช่วยทำให้พวก Linter มันเข้าใจว่ามันควรจะเป็นอะไร แล้วมันจะได้ให้ Warning ออกมาให้เราเห็นได้นั่นเอง

import datetime

def calculate_age (birth_year: int) -> int :
	return datetime.datetime.now().year - brith_year

จากตัวอย่างเดิมคือ Function ในการคำนวณอายุ เราเพิ่มที่บรรทัดที่ประกาศ Function เข้าไป จะเห็นว่าตรง birth_year เรามีการเติม Data Type ต่อท้ายไปด้วย บอกว่า birth_year ที่ใส่เข้ามาต้องเป็น Integer เท่านั้นนะ และ ตรงท้าย ๆ เลย -> int เป็นการบอกว่า Return Type ก็ต้องเป็น Integer ด้วยเช่นกัน

from typing import Any, NoReturn

def function_a (a : Any) -> NoReturn:
	// do sth

หรือถ้าเราไม่อยากกำหนด Type อะไรเลย เราก็สามารถเขียนบอกไว้ได้โดยการใช้ Any ตาม Code ด้านบนได้เลย อันไหนที่เราอยากให้เป็นเป็นอะไรก็ได้ เราก็ใส่ Any ไป ซึ่งเอาจริง ๆ เวลาเราเขียน เราก็ไม่ชอบนะ ที่จะใช้มัน

อีกอย่างที่เราเห็นในตัวอย่างนี้คือ NoReturn มันเป็นตัวที่บอกว่า Function นี้จะไม่ Return อะไรกลับไปเลย จะใช้ใน Function อย่างเดียวนะ

a = 10
if type(a) == int :
	print("It is number")

ใน Python เรามามารถเช็ค Type ของตัวแปรได้ผ่าน Built-in Function ตามด้านบนเลย มันก็จะเช็ค Type ของตัวแปร และคืนค่ากลับมาเป็น Type Class อันใหม่กลับมาให้เรา ทำให้ Type ที่เราจะเช็คได้ ไม่ใช่แค่ Primitive Types เท่านั้น แต่เป็น Object จาก Class ได้ด้วย เช่นกัน หรือ ถ้าเรามี Type ที่มีความพิเศษ เราก็สามารถใช้ NewType ด้านล่างนี้ได้

from typing import NewType

UserId = NewType('UserId', int)
some_id = UserId(524313)

NewType เป็น Helper Function ใน Python ที่ทำให้เราสามารถสร้าง Type ใหม่ได้ จริง ๆ เรามองว่ามันเป็น Type Alias มากกว่า คือ เราจะต้องบอกว่ามันคือ Type อะไร เช่นอันนี้เราสร้าง Type ชื่อ UserId ขึ้นมา เราบอกว่า มันคือ Int นะ ทำให้ เวลาเราเขียนจริง ๆ เราไม่ต้องจำว่า UserId คือ Type อะไร เราก็เรียก UserId แปะลงไปได้เลย เราว่าอันนี้ทำให้การเขียนง่ายขึ้นเยอะ

สรุป

เรื่องทั้งหมดที่เราเล่ามาในวันนี้ ไม่ได้ทำให้โปรแกรมเราทำงานได้เร็วขึ้น หรือมีอะไรดีขึ้นกับ User โดยตรง แต่มันเป็นผลดีกับฝั่ง Developer มากกว่า ที่ทำให้ลดความผิดพลาด และ เวลาในการนั่งหาจุดที่ผิด กับเรื่องอย่างการใช้ Typing ที่ผิด จนทำให้โปรแกรท Error ตอนทดสอบ หรือ ที่หนักกว่าคือ ไม่ Error แต่ให้ค่าออกมาผิด อันนี้แหละหายนะของจริงเลยละ ใช้เถอะนะ แล้วชีวิตจะดีขึ้น จบ สวัสดี

ส่วนพวก Linter ถ้าเราใช้พวก TextEditor ที่เป็นที่นิยมหลาย ๆ ตัว ก็จะมี Plugin ที่เป็น Linter สำหรับเช็ค Syntax และ Typing อยู่แล้วละ แต่ถ้าใครที่มาเป็น Command Line ตรง ๆ เลย ก็จะมี Tool หลายตัวที่เข้ามาทำหน้าที่ได้เหมือนกัน เช่น Mypy, Pytype และ PyRe