ডেটা সায়েন্সে পাইথনের সাধারণ সিনট্যাক্স (উন্নত স্তর)

এই ক’দিন ধরে আমি এই বইটি পড়ছি Data Science from Scrach (PDF ঠিকানা)। এটি ডেটা সায়েন্সের জন্য একটি চমৎকার, সহজবোধ্য সূচনা গ্রন্থ। এর একটি অধ্যায়ে পাইথনের মৌলিক সিনট্যাক্স এবং ডেটা সায়েন্সে ব্যবহৃত উন্নত সিনট্যাক্স সম্পর্কে আলোচনা করা হয়েছে। আমার মনে হয়েছে এটি খুব ভালোভাবে, সংক্ষেপে এবং স্পষ্টভাবে ব্যাখ্যা করা হয়েছে, তাই আমি এটি এখানে অনুবাদ করে রাখছি, যাতে পরে কাজে লাগে। ডেটা সায়েন্সে ব্যবহৃত পাইথন সিনট্যাক্স (প্রাথমিক স্তর) ডেটা সায়েন্সে ব্যবহৃত পাইথন সিনট্যাক্স (উন্নত স্তর)

এই অধ্যায়ে মূলত ডেটা প্রসেসিংয়ে অত্যন্ত কার্যকর পাইথনের কিছু উন্নত সিনট্যাক্স এবং বৈশিষ্ট্য আলোচনা করা হয়েছে (পাইথন 2.7 এর উপর ভিত্তি করে)।

সর্টিং Sorting

যদি আপনি পাইথনের কোনো লিস্টকে সর্ট করতে চান, তাহলে সেই লিস্টের sort মেথডটি ব্যবহার করতে পারেন। তবে যদি আসল লিস্টটিকে অপরিবর্তিত রেখে একটি নতুন সর্টেড লিস্ট পেতে চান, তাহলে sorted ফাংশনটি ব্যবহার করতে পারেন:

x = [4,1,2,3]
y = sorted(x)       # y = [1,2,3,4], x অপরিবর্তিত থাকে
x.sort()            # এখন x = [1,2,3,4]

sort বা sorted ডিফল্টভাবে ছোট থেকে বড় ক্রমে লিস্টকে সর্ট করে।

যদি বড় থেকে ছোট ক্রমে সর্ট করতে চান, তাহলে reverse = True প্যারামিটারটি উল্লেখ করতে পারেন।

আপনি আপনার প্রয়োজন অনুযায়ী সর্টিং ফাংশন তৈরি করতে পারেন, যাতে নির্দিষ্ট কীওয়ার্ড অনুসারে লিস্টকে সর্ট করা যায়:

# পরম মান অনুসারে বড় থেকে ছোট ক্রমে সর্ট করুন
x = sorted([-4,1,-2,3], key=abs, reverse=True) # is [-4,3,-2,1]
# শব্দগুলি কতবার এসেছে তার সংখ্যা অনুসারে বড় থেকে ছোট ক্রমে সর্ট করুন
wc = sorted(word_counts.items(),
key=lambda (word, count): count,
reverse=True)

লিস্ট কম্প্রিহেনশন List Comprehensions

লিস্ট থেকে নির্দিষ্ট কিছু উপাদান নিয়ে একটি নতুন লিস্ট তৈরি করতে, অথবা বিদ্যমান উপাদানগুলির মান পরিবর্তন করতে, অথবা উভয় কাজই করতে, আমাদের প্রায়শই এমন পরিস্থিতির মুখোমুখি হতে হয়। পাইথনে এর জন্য প্রচলিত পদ্ধতি হলো লিস্ট কম্প্রিহেনশন (List Comprehensions):

even_numbers = [x for x in range(5) if x % 2 == 0]  # [0, 2, 4]
squares = [x * x for x in range(5)]                 # [0, 1, 4, 9, 16]
even_squares = [x * x for x in even_numbers]        # [0, 4, 16]

একইভাবে, আপনি লিস্টকে ডিকশনারি বা সেটে রূপান্তর করতে পারেন:

square_dict = { x : x * x for x in range(5) }       # { 0:0, 1:1, 2:4, 3:9, 4:16 }
square_set = { x * x for x in [1, -1] }             # { 1 }

যদি লিস্টের উপাদানগুলো ব্যবহার করার প্রয়োজন না হয়, তাহলে আন্ডারস্কোরকে (underscore) ভেরিয়েবল হিসেবে ব্যবহার করতে পারেন:

zeroes = [0 for _ in even_numbers] # লিস্ট even_numbers এর সমান দৈর্ঘ্য

লিস্ট কম্প্রিহেনশনে একাধিক for লুপ সমর্থন করে:

pairs = [(x, y)
    for x in range(10)
    for y in range(10)]    # মোট ১০০টি জোড়া: (0,0) (0,1) ... (9,8), (9,9)

পরের for লুপ আগের for লুপের ফলাফল ব্যবহার করতে পারে:

increasing_pairs = [(x, y)                      # শুধুমাত্র x < y এমন জোড়া থাকবে
                    for x in range(10)          # range(lo, hi) সমান
                    for y in range(x + 1, 10)]  # [lo, lo + 1, ..., hi - 1]

ভবিষ্যতে আমরা প্রায়শই লিস্ট কম্প্রিহেনশন ব্যবহার করব।

জেনারেটর এবং ইটারেটর Generators and Iterators

লিস্টের একটি সমস্যা হলো, এটি খুব সহজেই অনেক বড় হয়ে যেতে পারে। যেমন, range(1000000) একটি এক মিলিয়ন (দশ লক্ষ) উপাদানের লিস্ট তৈরি করবে। যদি একবারে একটি করে ডেটা প্রসেস করা হয়, তাহলে অনেক সময় লাগতে পারে (বা মেমরি শেষ হয়ে যেতে পারে)। কিন্তু বাস্তবে আপনার হয়তো কেবল প্রথম কয়েকটি ডেটারই প্রয়োজন, সেক্ষেত্রে বাকি গণনাগুলো অপ্রয়োজনীয় হয়ে পড়ে।

অন্যদিকে, জেনারেটর (generator) আপনাকে কেবল প্রয়োজনীয় ডেটাগুলোই পুনরাবৃত্তি করতে সাহায্য করে। একটি ফাংশন এবং yield এক্সপ্রেশন ব্যবহার করে একটি জেনারেটর তৈরি করা যায়:

def lazy_range(n):
    """range এর একটি লেজি সংস্করণ"""
    i = 0
    while i < n:
        yield i
        i += 1

অনুবাদক কর্তৃক সংযোজনী: জেনারেটরও এক ধরনের বিশেষ ইটারেটর। yield হলো জেনারেটরকে ইটারেট করার মূল চাবিকাঠি। এটি জেনারেটরের এক্সিকিউশনকে থামিয়ে এবং আবার শুরু করার একটি পয়েন্ট হিসেবে কাজ করে। yield এক্সপ্রেশনকে মান অ্যাসাইন করা যেতে পারে এবং এর মান ফেরতও দেওয়া যেতে পারে। যে কোনো ফাংশন যা yield স্টেটমেন্ট ধারণ করে, তাকে জেনারেটর বলা হয়। জেনারেটর থেকে বের হওয়ার সময়, এটি বর্তমান এক্সিকিউশন স্টেট সংরক্ষণ করে এবং পরবর্তী এক্সিকিউশনের সময় তা পুনরায় শুরু করে, যাতে পরবর্তী ইটারেশন মান পাওয়া যায়। লিস্ট ইটারেশন প্রচুর মেমরি স্পেস দখল করে, কিন্তু জেনারেটর ব্যবহার করলে প্রায় একটি মেমরি স্পেসই ব্যবহার হয়, যার ফলে মেমরি সাশ্রয় হয়।

নিচের এই লুপটি yield থেকে একে একে মান গ্রহণ করবে যতক্ষণ না সব মান নিঃশেষিত হয়:

for i in lazy_range(10):
    do_something_with(i)

(আসলে, পাইথনে _lazy_range_ এর মতো একটি ফাংশন বিল্ট-ইন রয়েছে, যা xrange নামে পরিচিত। পাইথন 3-তে range ফাংশনটি xrange এর মতো কাজ করে।) এর মানে হলো আপনি একটি অসীম ক্রম তৈরি করতে পারেন:

def natural_numbers():
    """১, ২, ৩, ... ফেরত দেয়"""
    n = 1
    while True:
        yield n
        n += 1

তবে লুপ থেকে বের হওয়ার কোনো লজিক ছাড়া এমন স্টেটমেন্ট ব্যবহার করা উচিত নয়।

TIP

জেনারেটর ব্যবহার করে পুনরাবৃত্তি করার একটি অসুবিধা হলো, এটি প্রথম থেকে শেষ পর্যন্ত উপাদানগুলিকে কেবল একবারই পুনরাবৃত্তি করতে পারে। যদি একাধিকবার পুনরাবৃত্তি করতে চান, তাহলে আপনাকে প্রতিবার নতুন জেনারেটর তৈরি করতে হবে অথবা লিস্ট ব্যবহার করতে হবে।

জেনারেটর তৈরির দ্বিতীয় পদ্ধতি: বন্ধনীর মধ্যে কম্প্রিহেনশন এক্সপ্রেশন ব্যবহার করে:

lazy_evens_below_20 = (i for i in lazy_range(20) if i % 2 == 0)

আমরা জানি যে ডিকশনারির items() মেথডটি ডিকশনারির সমস্ত কী-ভ্যালু জোড়ার একটি লিস্ট ফেরত দেয়। তবে বেশিরভাগ ক্ষেত্রে, আমরা iteritems() জেনারেটর মেথডটি ইটারেশন করার জন্য ব্যবহার করি, যা প্রতিবার কেবল একটি কী-ভ্যালু জোড়া তৈরি করে এবং ফেরত দেয়।

র্যান্ডমনেস Randomness

ডেটা সায়েন্স শেখার সময়, আমাদের প্রায়শই র্যান্ডম সংখ্যা তৈরি করার প্রয়োজন হবে। এজন্য random মডিউলটি ইম্পোর্ট করলেই এটি ব্যবহার করা যাবে:

import random
four_uniform_randoms = [random.random() for _ in range(4)]
# [0.8444218515250481,        # random.random() র্যান্ডম সংখ্যা তৈরি করে
# 0.7579544029403025,         # র্যান্ডম সংখ্যা ০ থেকে ১ এর মধ্যে স্বাভাবিক করা হয়
# 0.420571580830845,          # এই ফাংশনটি র্যান্ডম সংখ্যা তৈরির জন্য সবচেয়ে বেশি ব্যবহৃত হয়
# 0.25891675029296335]

যদি আপনি পুনরাবৃত্তিযোগ্য (reproducible) ফলাফল পেতে চান, তাহলে random মডিউলকে random.seed দ্বারা সেট করা অভ্যন্তরীণ অবস্থার উপর ভিত্তি করে ছদ্ম-র্যান্ডম (অর্থাৎ, ডিটারমিনিস্টিক) সংখ্যা তৈরি করতে দিতে পারেন:

random.seed(10)           # সীড ১০ এ সেট করা হলো
print random.random()     # ০.৫৭১৪০২৫৯৪৬৯
random.seed(10)           # সীড আবার ১০ এ সেট করা হলো
print random.random()     # ০.৫৭১৪০২৫৯৪৬৯ আবার

মাঝে মাঝে আমরা random.randrange ফাংশনটিও ব্যবহার করি একটি নির্দিষ্ট পরিসরের মধ্যে র্যান্ডম সংখ্যা তৈরি করার জন্য:

random.randrange(10)      # range(10) = [0, 1, ..., 9] থেকে একটি র্যান্ডম সংখ্যা বেছে নিন
random.randrange(3, 6)    # range(3, 6) = [3, 4, 5] থেকে একটি র্যান্ডম সংখ্যা বেছে নিন

কিছু পদ্ধতি আছে যা মাঝে মাঝে ব্যবহার করতে খুব সুবিধাজনক। যেমন, random.shuffle একটি লিস্টের উপাদানগুলির ক্রম এলোমেলো করে একটি নতুন র্যান্ডমভাবে সাজানো লিস্ট তৈরি করে:

up_to_ten = range(10)
random.shuffle(up_to_ten)
print up_to_ten
# [2, 5, 1, 9, 7, 3, 8, 6, 4, 0] (আপনার ফলাফল ভিন্ন হতে পারে)

যদি একটি লিস্ট থেকে একটি র্যান্ডম উপাদান বেছে নিতে চান, তাহলে random.choice মেথডটি ব্যবহার করতে পারেন:

my_best_friend = random.choice(["Alice", "Bob", "Charlie"]) # আমি পেয়েছি "Bob"

যদি একটি র্যান্ডম সিকোয়েন্স তৈরি করতে চান কিন্তু আসল লিস্টটিকে এলোমেলো করতে না চান, তাহলে random.sample মেথডটি ব্যবহার করতে পারেন:

lottery_numbers = range(60)
winning_numbers = random.sample(lottery_numbers, 6) # [১৬, ৩৬, ১০, ৬, ২৫, ৯]

আপনি একাধিকবার কল করে একাধিক র্যান্ডম নমুনা নির্বাচন করতে পারেন (পুনরাবৃত্তি অনুমোদিত):

four_with_replacement = [random.choice(range(10))
                         for _ in range(4)]
# [৯, ৪, ৪, ২]

রেগুলার এক্সপ্রেশন Regular Expressions

রেগুলার এক্সপ্রেশন (Regular Expressions) টেক্সট অনুসন্ধানের জন্য ব্যবহৃত হয়। এটি কিছুটা জটিল হলেও অত্যন্ত কার্যকর, তাই রেগুলার এক্সপ্রেশন নিয়ে অনেক বই লেখা হয়েছে। আমরা যখন এগুলোর সম্মুখীন হব, তখন বিস্তারিত ব্যাখ্যা করব। নিচে পাইথনে রেগুলার এক্সপ্রেশন ব্যবহারের কিছু উদাহরণ দেওয়া হলো:

import re
print all([                                 # নিচের সব স্টেটমেন্ট 'true' ফেরত দেবে, কারণ
    not re.match("a", "cat"),               # * 'cat' 'a' দিয়ে শুরু হয় না
    re.search("a", "cat"),                  # * 'cat' এর মধ্যে 'a' অক্ষরটি আছে
    not re.search("c", "dog"),              # * 'dog' এর মধ্যে 'c' অক্ষরটি নেই
    3 == len(re.split("[ab]", "carbs")),    # * 'a' বা 'b' এর উপর ভিত্তি করে শব্দটি তিনটি অংশে বিভক্ত করা হয়েছে ['c','r','s']
    "R-D-" == re.sub("[0-9]", "-", "R2D2")  # * সংখ্যাগুলিকে হাইফেন দিয়ে প্রতিস্থাপন করা হয়েছে
    ])                                      # আউটপুট True

অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং Object-Oriented Programming

অন্যান্য অনেক ভাষার মতোই, পাইথন আপনাকে ডেটা এনক্যাপসুলেট (encapsulate) করার জন্য ক্লাস এবং সেগুলোর উপর অপারেশন চালানোর জন্য ফাংশন সংজ্ঞায়িত করার সুযোগ দেয়। আমরা মাঝে মাঝে আমাদের কোডকে আরও পরিষ্কার এবং সংক্ষিপ্ত করার জন্য এগুলি ব্যবহার করি। প্রচুর মন্তব্য সহ একটি উদাহরণ তৈরি করে এগুলি ব্যাখ্যা করা সবচেয়ে সহজ হতে পারে। ধরা যাক, পাইথনে বিল্ট-ইন সেট (Set) নেই, সেক্ষেত্রে আমরা হয়তো আমাদের নিজস্ব Set ক্লাস তৈরি করতে চাইব। তাহলে এই ক্লাসটির কী কী বৈশিষ্ট্য থাকা উচিত? উদাহরণস্বরূপ, একটি Set দেওয়া হলে, আমাদের তাতে আইটেম যোগ করতে, আইটেম সরাতে, এবং এটি একটি নির্দিষ্ট মান ধারণ করে কিনা তা পরীক্ষা করতে সক্ষম হওয়া দরকার। সুতরাং, আমরা এই সমস্ত কার্যকারিতাগুলিকে ক্লাসটির মেম্বার ফাংশন হিসেবে তৈরি করব। এভাবে, আমরা Set অবজেক্টের পরে ডট (dot) ব্যবহার করে এই মেম্বার ফাংশনগুলি অ্যাক্সেস করতে পারব:

# প্রথা অনুযায়ী, আমরা _PascalCase_ ক্লাসের নাম দেব
class Set:
    # এগুলো মেম্বার ফাংশন
    # প্রতিটি মেম্বার ফাংশনের শুরুতে একটি "self" প্যারামিটার থাকে (আরেকটি প্রথা)
    # "self" ব্যবহৃত নির্দিষ্ট Set অবজেক্টটিকে বোঝায়

    def __init__(self, values=None):
        """এটি কনস্ট্রাক্টর ফাংশন
        যখনই আপনি একটি নতুন Set তৈরি করবেন, এই ফাংশনটি কল করা হবে
        এভাবে কল করা যেতে পারে
        s1 = Set() # খালি সেট
        s2 = Set([1,2,2,3]) # নির্দিষ্ট মান দিয়ে সেট শুরু করুন"""
        self.dict = {} # Set-এর প্রতিটি ইনস্ট্যান্সের নিজস্ব dict অ্যাট্রিবিউট থাকে
        # আমরা এই অ্যাট্রিবিউটটি প্রতিটি মেম্বারকে ট্র্যাক করতে ব্যবহার করি
        if values is not None:
            for value in values:
            self.add(value)

    def __repr__(self):
        """এটি Set অবজেক্টের স্ট্রিং রিপ্রেজেন্টেশন
        আপনি পাইথন কমান্ড উইন্ডোতে স্ট্রিং টাইপ করে অথবা str() মেথড ব্যবহার করে অবজেক্টে স্ট্রিং পাস করে এটি দেখতে পারেন"""
        return "Set: " + str(self.dict.keys())

    # আমরা self.dict এর মধ্যে কী হিসেবে যোগ করে এবং এর মান True সেট করে সদস্যপদ বোঝাব
    def add(self, value):
        self.dict[value] = True

    # যদি প্যারামিটারটি ডিকশনারির একটি কী হয়, তাহলে সংশ্লিষ্ট মানটি Set এর মধ্যে আছে
    def contains(self, value):
        return value in self.dict

    def remove(self, value):
        del self.dict[value]

তারপর আমরা Set কে এভাবে ব্যবহার করতে পারি:

s = Set([1,2,3])
s.add(4)
print s.contains(4)     # True
s.remove(3)
print s.contains(3)     # False

ফাংশনাল টুলস Functional Tools

আংশিক ফাংশন partial

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

def exp(base, power):
    return base ** power

আমরা এটিকে ব্যবহার করে এমন একটি ফাংশন তৈরি করতে চাই, যা একটি ভেরিয়েবল ইনপুট নেবে এবং exp(2, power) অর্থাৎ ২ কে ভিত্তি ধরে পাওয়ার ফাংশনের ফলাফল আউটপুট দেবে।

অবশ্যই, আমরা def ব্যবহার করে একটি নতুন ফাংশন সংজ্ঞায়িত করতে পারি, যদিও এটি খুব বুদ্ধিমানের কাজ মনে নাও হতে পারে:

def two_to_the(power):
  return exp(2, power)

আরও বুদ্ধিমানের কাজ হলো functools.partial মেথডটি ব্যবহার করা:

from functools import partial
two_to_the = partial(exp, 2)      # বর্তমান ফাংশনটির কেবল একটি ভেরিয়েবল আছে
print two_to_the(3)               # ৮

যদি নাম উল্লেখ করা হয়, তাহলে partial মেথডটি অন্যান্য প্যারামিটার পূরণ করতেও ব্যবহার করা যেতে পারে:

square_of = partial(exp, power=2)
print square_of(3)                # ৯

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

ম্যাপ map

আমরা মাঝে মাঝে map, reduce, এবং filter এর মতো ফাংশনগুলি লিস্ট কম্প্রিহেনশনের বিকল্প হিসেবে ব্যবহার করি:

def double(x):
    return 2 * x

xs = [1, 2, 3, 4]
twice_xs = [double(x) for x in xs]      # [2, 4, 6, 8]
twice_xs = map(double, xs)              # একই ফল
list_doubler = partial(map, double)     # ফাংশনের কাজ হলো লিস্টকে দ্বিগুণ করা
twice_xs = list_doubler(xs)             # এটাও [2, 4, 6, 8]

map মেথডটি একাধিক প্যারামিটার সহ ফাংশন থেকে একাধিক লিস্টে ম্যাপ করার জন্যও ব্যবহার করা যেতে পারে:

def multiply(x, y): return x * y

products = map(multiply, [1, 2], [4, 5])  # [১ * ৪, ২ * ৫] = [৪, ১০]

ফিল্টার filter

একইভাবে, filter লিস্ট কম্প্রিহেনশনের if এর কার্যকারিতা বাস্তবায়ন করে:

def is_even(x):
    """যদি x জোড় হয় তবে True, বিজোড় হলে False ফেরত দেয়"""
    return x % 2 == 0

x_evens = [x for x in xs if is_even(x)]   # [2, 4]
x_evens = filter(is_even, xs)             # একই ফল
list_evener = partial(filter, is_even)    # এই ফাংশনটি ফিল্টার করার কাজ করে
x_evens = list_evener(xs)                 # এটাও [2, 4]

রিডিউস reduce

reduce মেথডটি লিস্টের প্রথম এবং দ্বিতীয় উপাদানকে একত্রিত করে, তারপর ফলাফলটিকে তৃতীয় উপাদানের সাথে একত্রিত করে, এবং এই প্রক্রিয়াটি একটি একক ফলাফল না পাওয়া পর্যন্ত পুনরাবৃত্তি করতে থাকে:

x_product = reduce(multiply, xs)          # = ১ * ২ * ৩ * ৪ = ২৪
list_product = partial(reduce, multiply)  # এই ফাংশনটি একটি লিস্টকে সংকুচিত করার কাজ করে
x_product = list_product(xs)              # এটাও ২৪

এনিউমারেট enumerate

মাঝে মাঝে এমন পরিস্থিতি দেখা যায় যেখানে একটি লিস্টের উপাদান এবং তার ইনডেক্স উভয়ই একযোগে ব্যবহার করার প্রয়োজন হয়:

# পাইথন-সুলভ নয় (বেশি সংক্ষিপ্ত বা মার্জিত নয়)
for i in range(len(documents)):
    document = documents[i]
    do_something(i, document)

# একইভাবে পাইথন-সুলভ নয় (বেশি সংক্ষিপ্ত বা মার্জিত নয়)
i = 0
for document in documents:
    do_something(i, document)
    i += 1

সবচেয়ে সংক্ষিপ্ত উপায় হলো enumerate মেথড ব্যবহার করে tuples (index, element) তৈরি করা:

for i, document in enumerate(documents):
    do_something(i, document)

একইভাবে, যদি কেবল ইনডেক্স ব্যবহার করতে চান:

for i in range(len(documents)): do_something(i)   # সংক্ষিপ্ত নয়
for i, _ in enumerate(documents): do_something(i) # সংক্ষিপ্ত

পরে আমরা এই পদ্ধতিটি প্রায়শই ব্যবহার করব।

জিপ এবং আর্গুমেন্ট আনপ্যাক করা zip and Argument Unpacking

জিপ zip

আমরা প্রায়শই দুই বা ততোধিক লিস্টকে ‘জিপ’ করি। জিপিং আসলে একাধিক লিস্টকে তাদের সংশ্লিষ্ট টুপলের একটি একক লিস্টে রূপান্তরিত করা:

list1 = ['a', 'b', 'c']
list2 = [1, 2, 3]
zip(list1, list2)       # ফলাফল [('a', 1), ('b', 2), ('c', 3)]

আর্গুমেন্ট আনপ্যাক করা Argument Unpacking

যদি একাধিক লিস্টের দৈর্ঘ্য অসঙ্গতিপূর্ণ হয়, তাহলে জিপিং প্রক্রিয়াটি সবচেয়ে ছোট লিস্টের শেষে থেমে যাবে। আপনি একটি অদ্ভুত “আনজিপ” আনপ্যাকিং কৌশল ব্যবহার করে লিস্টকে আনজিপ করতে পারেন:

pairs = [('a', 1), ('b', 2), ('c', 3)]
letters, numbers = zip(*pairs)

এখানে স্টারিক (asterisk) আর্গুমেন্ট আনপ্যাকিংয়ের জন্য ব্যবহৃত হয়, যা pairs এর উপাদানগুলিকে zip এর একক আর্গুমেন্ট হিসেবে ব্যবহার করে। নিচের কলিং পদ্ধতিটি একই প্রভাব ফেলে:

zip(('a', 1), ('b', 2), ('c', 3))  # ফেরত দেয় [('a','b','c'), ('1','2','3')]

আর্গুমেন্ট আনপ্যাকিং অন্যান্য ফাংশনের সাথেও ব্যবহার করা যেতে পারে:

def add(a, b): return a + b

add(1, 2)           # ফেরত দেয় ৩
add([1, 2])         # এরর দেখায়
add(*[1, 2])        # ফেরত দেয় ৩

যদিও এটি খুব বেশি ব্যবহারিক নয়, তবে কোডকে সংক্ষিপ্ত করার জন্য এটি একটি চমৎকার কৌশল।

অনির্দিষ্ট দৈর্ঘ্যের আর্গুমেন্ট পাস করা args and kwargs

ধরা যাক, আমরা একটি উচ্চ-ক্রম ফাংশন (higher-order function) তৈরি করতে চাই, যা একটি পুরোনো ফাংশন ইনপুট হিসেবে নেবে এবং একটি নতুন ফাংশন ফেরত দেবে, যা পুরোনো ফাংশনের ফলাফলকে ২ দিয়ে গুণ করবে:

def doubler(f):
    def g(x):
      return 2 * f(x)
    return g

উদাহরণ চালান:

def f1(x):
    return x + 1

g = doubler(f1)
print g(3)        # ৮ (== ( ৩ + ১) * ২)
print g(-1)       # ০ (== (-১ + ১) * ২)

তবে, যদি একটার বেশি প্যারামিটার পাস করা হয়, তাহলে এই পদ্ধতিটি আর কার্যকর থাকে না:

def f2(x, y):
    return x + y

g = doubler(f2)
print g(1, 2) # এরর দেখায় TypeError: g() takes exactly 1 argument (2 given)

তাই আমাদের এমন একটি ফাংশন সংজ্ঞায়িত করতে হবে যা যে কোনো সংখ্যক প্যারামিটার গ্রহণ করতে পারে, এবং তারপর আর্গুমেন্ট আনপ্যাকিং ব্যবহার করে একাধিক প্যারামিটার পাস করতে হবে। এটি কিছুটা জাদুর মতো মনে হতে পারে:

def magic(*args, **kwargs):
    print "unnamed args:", args
    print "keyword args:", kwargs
magic(1, 2, key="word", key2="word2")
# আউটপুট:
# unnamed args: (1, 2)
# keyword args: {'key2': 'word2', 'key': 'word'}

যখন আমরা এভাবে একটি ফাংশন সংজ্ঞায়িত করি, তখন args (arguments এর সংক্ষিপ্ত রূপ) হলো নামহীন প্যারামিটারগুলির একটি টুপল, আর kwargs (keyword arguments এর সংক্ষিপ্ত রূপ) হলো নামযুক্ত প্যারামিটারগুলির একটি ডিকশনারি।

এগুলি এমন পরিস্থিতিতেও ব্যবহার করা যেতে পারে যেখানে পাস করা প্যারামিটারগুলি লিস্ট (বা টুপল) বা অ্যারে আকারে থাকে:

def other_way_magic(x, y, z):
    return x + y + z

x_y_list = [1, 2]
z_dict = { "z" : 3 }
print other_way_magic(*x_y_list, **z_dict)    # ৬

আপনি এটিকে বিভিন্ন অদ্ভুত উপায়ে ব্যবহার করতে পারেন, তবে আমরা এটিকে কেবল উচ্চ-ক্রম ফাংশনে অনির্দিষ্ট দৈর্ঘ্যের প্যারামিটার পাস করার সমস্যা সমাধানের জন্য ব্যবহার করব:

def doubler_correct(f):
    """f যাই হোক না কেন, এটি কার্যকরভাবে কাজ করে"""
    def g(*args, **kwargs):
        """প্যারামিটার সংখ্যা যাই হোক না কেন, এই ফাংশনটি সঠিকভাবে প্যারামিটার f এ পাস করতে পারে"""
        return 2 * f(*args, **kwargs)
    return g

g = doubler_correct(f2)
print g(1, 2) # ৬

ডেটা সায়েন্সের জগতে স্বাগতম!

টিং! অভিনন্দন, আপনি ডেটা সায়েন্সের এক নতুন জগতের দ্বার উন্মোচন করেছেন! এবার আপনি আনন্দের সাথে কাজ শুরু করতে পারেন!

সম্পর্কিত পড়া:

ডেটা সায়েন্সে ব্যবহৃত পাইথন সিনট্যাক্স (প্রাথমিক স্তর)