টাইপ হিন্টিং

টাইপ হিন্টিং #

টাইপ হিন্টিং পাইথনের একেবারেই নতুন একটি ধারণা। এটা অনেকটা ভ্যারিয়েবল বা ফাংশন আর্গুমেন্টের টাইপ ডিক্লেয়ার করে দেয়ার মত। পাইথনে কোন ভ্যারিয়েবলের ডেটা টাইপ নির্ধারণ করে দেয়া লাগে না। ভ্যালু অ্যাসাইন করলে ভ্যারিয়েবলের ডেটা টাইপ আপনা-আপনি সেট হয়ে যায় - এটা আমরা ইতিমধ্যেই জেনেছি।

তাহলে আমাদের শিশুসুলভ মনে এ প্রশ্ন উঠতেই পারে, টাইপ হিন্টিং আবার কি জিনিস! একটা সংজ্ঞা তো আমরা উপরেই জেনেছি। কিন্তু সংজ্ঞায় কি আর প্রোগ্রামারদের মন ভরে? তো হয়ে যাক একটা উদাহরণ।

def add(a: int, b: int) -> int:
    c = a + b
    return c

উপরের ফাংশনটা একটু কিম্ভুতকিমাকার লাগছে, তাই না? আসলে এতক্ষণ আমরা যত ইউজার-ডিফাইন্ড ফাংশন দেখে এসেছি, তার কোনটার সুরতই এরকম নয়। সুরত ভিন্ন হবার কারণও রয়েছে। এই ফাংশনে আমরা টাইপ হিন্টিং ব্যবহার করেছি। টাইপ হিন্টিং করে বলে দিয়েছি a ও b আর্গুমেন্ট দুটি ইন্টিজার টাইপের হবে। আবার এটাও বলে দিয়েছি যে, add() ফাংশনটা যা রিটার্ন করবে তাও ইন্টিজার টাইপের হবে। যাহোক, এবার আরেকটা উদাহরণ দেখা যাক।

def add(a: int, b: int) -> int:
    print(a, type(a))
    print(b, type(b))
    c = a + b
    print(c, type(c))
    return c

add(2, 3)

আউটপুট

2 <class 'int'>
3 <class 'int'>
5 <class 'int'>

এই উদাহরণের add() ফাংশনে আমরা দুটি ইন্টিজার টাইপের ভ্যালু আর্গুমেন্ট হিসেবে পাস করায় ফাংশনটি একটি ইন্টিজার টাইপের ভ্যালু রিটার্ন করেছে। একেবারে টাইপ হিন্টিংয়ে যেমনটা বলা ছিল, ঘটনা তেমনই ঘটেছে। ঠিক আছে! এবার আরো একটা উদাহরণ দেখা যাক।

def add(a: int, b: int) -> int:
    print(a, type(a))
    print(b, type(b))
    c = a + b
    print(c, type(c))
    return c

add('Bangla', 'desh')

আউটপুট

Bangla <class 'str'>
desh <class 'str'>
Bangladesh <class 'str'>

এবার ঘটনা একটু রহস্যজনক। আর্গুমেন্ট হিসেবে ফাংশনের নেয়ার কথা ছিল ইন্টিজার টাইপের ভ্যালু অথচ আমরা পাস করেছি স্ট্রিং টাইপের ভ্যালু। আবার ফাংশনের রিটার্ন করার কথা ছিল ইন্টিজার টাইপের ভ্যালু অথচ রিটার্ন করেছে স্ট্রিং টাইপের ভ্যালু। আর সবচেয়ে আশংকাজনক কথা হল, পাইথন কোন এরর থ্রো করেনি। এই রহস্যের সমাধান কি করা যাবে? চেষ্টা করা যাক!

টাইপ হিন্টিং করলে আসলে রানটাইমে টাইপ চেক হয় না। এটা আসলে অ্যানোটেশন। আবার ডকুমেন্টেশনও বলতে পারি। ডকুমেন্টেশন এই অর্থে বললাম যে, add() ফাংশনের প্রথম লাইনের দিকে তাকিয়েই আমরা বলতে পারি a ও b ভ্যারিয়েবলের প্রত্যাশিত টাইপ হচ্ছে ইন্টিজার এবং ফাংশনটার প্রত্যাশিত রিটার্ন টাইপও ইন্টিজার। ব্যাপারটা হয়ত এখন আমাদের অনেকের কাছেই খুব একটা দরকারি মনে হচ্ছে না। কিন্তু যখন অন্য কোন ব্যক্তি আমাদের প্রোগ্রাম পড়বেন, তিনি কিন্তু খুব সহজেই বুঝতে পারবেন কোথায় কি ব্যবহার করে কি ফল পাওয়া যাবে।

আচ্ছা, পাইথনের জন্য কোন স্টাটিক টাইপ চেকার কি আসলেই নেই? এই প্রশ্নের উত্তরে না বললে মাইপাই প্রজেক্টের বদনাম করা হয়ে যাবে। মাইপাই হল পাইথনের (ঐচ্ছিক) স্টাটিক টাইপ চেকার। তবে এটা বিল্ট-ইন নয়। আমাদেরকে আলাদাভাবে ইন্সটল করে নিতে হবে। আর হ্যাঁ, এটা কিন্তু এখনও ডেভেলপমেন্ট ফেইজে রয়েছে।

$ sudo pip3 install mypy

এবার মাইপাই দিয়ে একটু আগে লেখা প্রোগ্রামটা চেক করা যাক। ধরে নিচ্ছি, প্রোগ্রামটা আমরা type_hint.py ফাইলে লিখেছি।

$ mypy type_hint.py

আউটপুট

type_hint.py:8: error: Argument 1 to "add" has incompatible type "str"; expected "int"
type_hint.py:8: error: Argument 2 to "add" has incompatible type "str"; expected "int"

মাইপাই আমাদের চমৎকারভাবে বলে দিচ্ছে যে, আমাদের প্রোগ্রামের আট নম্বর লাইনের add() ফাংশনে ঘাপলা আছে। আর‌ো বলে দিচ্ছে যে, আমরা a ও b আর্গুমেন্টে ইন্টিজার টাইপের বদলে স্ট্রিং টাইপের ভ্যালু পাস করেছি।

ভ্যারিয়েবল অ্যানোটেশন #

টাইপ হিন্টিং সম্পর্কে ধারণা নিতে গিয়ে আমরা এক দৌড়ে একেবারে সাগরে গিয়ে পড়েছি। তাই এবার একটু পিছনে ফিরে তাকাব আর ধাপে ধাপে সাঁতার শেখার চেষ্টা করব। প্রথমেই ভ্যারিয়েবল অ্যানোটেশন দিয়ে শুরু করা যাক।

number: int = 10
value = 3.1415 # type: float
name: str = 'maateen'
address = 'Dhaka' # type: str
is_happy: bool = True
is_sad = False #type: bool

উপরের উদাহরণ থেকে আমরা বুঝতে পারছি যে, ভ্যারিয়েবল অ্যানোটেশন দুইভাবে করা সম্ভব। এই অধ্যায়ের শুরুর দিকে ফাংশনের আর্গুমেন্টকে যেভাবে অ্যানোটেট করেছি, তার পাশাপাশি কমেন্ট করেও ভ্যারিয়েবল অ্যানোটেশন সম্ভব। এই উদাহরণে আমরা ইন্টিজার, ফ্লোট, স্ট্রিং ও বুলিয়ান টাইপের ভ্যারিয়েবল অ্যানোটেট করেছি। এবার লিস্ট, টাপল ও ডিকশনারি টাইপের ভ্যারিয়েবল অ্যানোটেট করার চেষ্টা করা যাক।

numbers: tuple[int]
planets = ['Earth', 'Mars', 'Jupiter'] # type: list[str]

মাইপাই দিয়ে চেক করলে আমরা নিচের মত দুটি এরর দেখতে পাব।

type_hint.py:1: error: "tuple" is not subscriptable, use "typing.Tuple" instead
type_hint.py:2: error: "list" is not subscriptable, use "typing.List" instead

এই এররগুলো কি বলতে চাচ্ছে? উত্তরটা খুব সিম্পল – টাইপিং মডিউল ব্যবহার করতে বলছে। পাইথনের ৩.৫ সংস্করণ থেকে এই মডিউলটি স্টান্ডার্ড লাইব্রেরিতে প্রভিশনার বেসিসে যোগ করা হয়েছে। প্রভিশনাল বেসিস বলতে বুঝায় যার কোন ব্যাকওয়ার্ড কম্প্যাটিবিলিটি গ্যারান্টি নাই। যাহোক, আমরা আমাদের গল্পে ফিরে আসি।

from typing import Dict, List, Tuple

numbers: Tuple[int]
planets = ['Earth', 'Mars', 'Jupiter'] # type: List[str]
books: Dict[str, str]

টাইপিং মডিউল Dict, List, Tuple, Any, Union ইত্যাদি টাইপ হিন্টিং সাপোর্ট করে। Any মানে হল টাইপের কোন বাদ-বিচার নাই, স্ট্রিং-ও হতে পারে আবার ইন্টিজারও হতে পারে। ListTuple-এর ক্ষেত্রে তাতে কোন ধরনের ভ্যারিয়েবল থাকবে তাও বলে দিতে হয়। আর Dict-এর ক্ষেত্রে কী ও ভ্যালু - উভয়ের টাইপই বলে দিতে হয়।

ক্লাস এবং ইন্সট্যান্স ভ্যারিয়েবল অ্যানোটেশন #

ক্লাস এবং ইন্সট্যান্স ভ্যারিয়েবল অ্যানোটেশন একদম সিম্পল জিনিস। একটা উদাহরণ দেখা যাক।

from typing import ClassVar

class Human:
    name: str
    age: int
    gender: str
    address: ClassVar[str] = 'Dhaka'

    def __init__(self, name: str = 'maateen') -> None:
        self.name = name

এই উদাহরণে name, age ও gender হল ইন্সট্যান্স ভ্যারিয়েবল আর address হল ক্লাস ভ্যারিয়েবল। ক্লাস ভ্যারিয়েবলকে টাইপিং মডিউলের ClassVar ক্লাস দিয়ে অ্যানোটেট করেছি আমরা। আরেকটা কথা, ClassVar ব্যবহার করার সময় এর প্যারামিটার হিসেবে কোন টাইপ ভ্যারিয়েবল ব্যবহার করা যাবে না।

টাইপ অ্যালিয়াস #

অ্যালিয়াস (alias)-এর বাংলা হল উপনাম বা ওরফে। আমরা অনেক সময় টিভিতে খবরে এরকমটা শুনে থাকি - র‍্যাবের সাথে বন্দুকযুদ্ধে কুখ্যাত সন্ত্রাসী মোহাম্মদ হুমায়ুন কবির ওরফে গালকাটা কবির নিহত। এখানে, মোহাম্মদ হুমায়ুন কবির উক্ত লোকের আসল নাম আর গালকাটা কবির হল তার উপনাম বা অ্যালিয়াস।

টাইপ অ্যালিয়াস হল কোন একটা টাইপকে অন্য নামে ডাকা অথবা আরো ভালভাবে বলতে গেলে, অন্য ভ্যারিয়েবলে অ্যাসাইন করা।

from typing import Dict, List

HostName = str
Address = str
Server = Dict[HostName, Address]
Network = List[Server]

উপরের উদাহরণটিতে HostName ও Address হল স্ট্রিংয়ের (str) টাইপ অ্যালিয়াস এবং Server হল ডিকশনারির (Dict) টাইপ অ্যালিয়াস যা আবার HostName ও Address এই দুটি টাইপের সমন্বয়ে গঠিত। সবশেষে, Network হল লিস্টের (List) টাইপ অ্যালিয়াস যাতে প্রত্যেকটি আইটেম আবার Server টাইপের।

নিউ টাইপ #

টাইপকে অন্য নামে ডাকাডাকি অনেক তো হল। এবার সময় নতুন টাইপ তৈরি করার। আর এজন্য আমাদেরকে সাহায্য করবে NewType() হেল্পার ফাংশন। এই ফাংশনটি দুটি প্যারামিটার নেয় - নতুন টাইপের নাম (name) এবং টাইপ (tp)। একটা উদাহরণ দেখা যাক।

from typing import Dict, List, NewType

HostName = NewType('HostName', str)
Address = NewType('Address', str)
Server = NewType('Server', Dict[HostName, Address])
Network = NewType('Network', List[Server])

টাইপ অ্যালিয়াসের উদাহরণটাই আমরা আবার নতুন মোড়কে দেখছি। এখানে আমরা HostName, Address, Server ও Network নামের নতুন কিছু টাইপ তৈরি করেছি। আচ্ছা, তৈরি তো করলাম। এবার ব্যবহার করা যাক।

hostname = HostName('local')
address = Address('127.0.0.1')
server = Server({hostname: address})
network = Network([server])

পূর্বের উদাহরণগুলোর তুলনায় একেবারেই ভিন্ন, তাই না? আচ্ছা, আগের মত করে টাইপগুলোকে ব্যবহার করলে কি সমস্যা হত? সেই তদন্তের দায়ভার শুধুই আপনাদের। ওহ! আরেকটা কথা, এই তদন্ত শেষ না করে সামনে আগানোতে শার্লক হোমসের নিষেধ আছে।

টাইপ ভ্যারিয়েবল #

টাইপ ভ্যারিয়েবল সম্পর্কে অল্প-বিস্তর জানার পূর্বে আমরা একটা উদাহরণ দেখব।

from typing import TypeVar

A = TypeVar('A')
B = TypeVar('B', str)
C = TypeVar('C', str, int)

def add(x: A, y: C) -> A:
    pass

আউটপুট

type_hint.py:4: error: TypeVar cannot have only a single constraint
type_hint.py:4: error: "object" not callable

আমরা ইতিমধ্যে str, int, bool প্রভৃতি টাইপের সাথে পরিচিত হয়েছি এবং কোন ভ্যারিয়েবলের সামনে কোলন চিহ্ন দিয়ে এদেরকে ব্যবহারও করেছি। এদেরকে জেনেরিক টাইপ বলা হয়। আর NewType() ফাংশন ব্যবহার করে আমরা যে নতুন টাইপ তৈরি করেছিলাম, তা কিন্তু জেনেরিক টাইপ ছিল না। এইজন্যই ভ্যারিয়েবলের সাথে কোলন চিহ্ন দিয়ে আমরা সেগুলোকে ব্যবহার করতে পারিনি।

এবার আমাদের উদাহরণে ফেরা যাক। TypeVar() ফাংশন দিয়ে আমরা নতুন জেনেরিক টাইপ তৈরি করতে পারি। আউটপুটে দেখতে পাচ্ছি চার নাম্বার লাইনে অর্থাৎ B = TypeVar('B', str) লাইনে দুটো এরর থ্রো করেছে মাইপাই। কারণটা হল, TypeVar() ফাংশনটি নতুন টাইপের নামের পাশাপাশি কমপক্ষে দুটি টাইপকে প্যারামিটার হিসেবে নিয়ে থাকে। আর যেহেতু আমরা চার নাম্বার লাইনে শুধু একটি (str) টাইপ পাস করেছি, তাই মাইপাই এরর থ্রো করেছে।

তবে এসবের পরেও ঝামেলা কিন্তু একটু রয়েছে। আর সেটা হল এই নতুন টাইপকে ব্যবহার করার ক্ষেত্রে। কিন্তু বইয়ের এই পর্যায়ে এসে এরকম আট-দশটা সমস্যার সমাধান তো আপনাদের থেকে আশা করাই যেতে পারে।

মন্তব্য করুন