سینتکس پرکاربرد پایتون در علم داده (پیشرفته)

این روزها مشغول مطالعه کتاب Data Science from Scratch (لینک 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)]    # در مجموع 100 جفت: (0,0), (0,1), ..., (9,8), (9,9)

حلقه for بعدی می‌تواند از نتایج حلقه for قبلی استفاده کند:

increasing_pairs = [(x, y)                      # فقط شامل جفت‌هایی که x < y هستند
                    for x in range(10)          # range(lo, hi) equals
                    for y in range(x + 1, 10)]  # [lo, lo + 1, ..., hi - 1]

در آینده، ما به طور مکرر از لیست کامپریهنشن استفاده خواهیم کرد.

ژنراتورها و ایتریتورها Generators and Iterators

یکی از مشکلاتی که لیست‌ها دارند این است که به راحتی می‌توانند بسیار بزرگ شوند؛ برای مثال، range(1000000) یک لیست با یک میلیون عنصر تولید می‌کند. اگر قرار باشد داده‌ها را یکی‌یکی پردازش کنیم، ممکن است زمان زیادی طول بکشد (یا حتی حافظه سیستم تمام شود). در حالی که ممکن است فقط به چند داده اول نیاز داشته باشید و بقیه عملیات اضافی باشند.

ژنراتورها به شما این امکان را می‌دهند که تنها بر روی داده‌هایی که به آن‌ها نیاز دارید، تکرار (iterate) کنید. می‌توانید با استفاده از یک تابع و عبارت yield یک ژنراتور ایجاد کنید:

def lazy_range(n):
    """یک نسخه تنبل از range"""
    i = 0
    while i < n:
        yield i
        i += 1

توضیح مترجم: ژنراتورها نیز نوعی ایتریتور خاص هستند و yield کلید اصلی پیاده‌سازی تکرار در آن‌هاست. yield به عنوان نقطه‌ای برای توقف و ازسرگیری اجرای ژنراتور عمل می‌کند؛ می‌توان به عبارت yield مقداری اختصاص داد یا مقدار آن را برگرداند. هر تابعی که شامل دستور yield باشد، یک ژنراتور نامیده می‌شود. هنگامی که اجرای یک ژنراتور متوقف می‌شود، وضعیت فعلی اجرای خود را ذخره کرده و در فراخوانی بعدی، آن وضعیت را بازیابی می‌کند تا مقدار تکراری بعدی را تولید کند. استفاده از لیست‌ها برای تکرار، فضای حافظه زیادی را اشغال می‌کند، در حالی که ژنراتورها تقریباً فضای یکسانی را اشغال می‌کنند و به این ترتیب در مصرف حافظه صرفه‌جویی می‌شود.

حلقه زیر هر بار یک مقدار از yield را مصرف می‌کند تا زمانی که تمام مقادیر مصرف شوند:

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

(در واقع، پایتون تابعی معادل lazy_range بالا را به صورت داخلی دارد که در پایتون 2 xrange نامیده می‌شود و در پایتون 3 به range تغییر نام یافته است.) این بدان معناست که می‌توانید یک دنباله بی‌نهایت ایجاد کنید:

def natural_numbers():
    """برمی‌گرداند 1, 2, 3, ..."""
    n = 1
    while True:
        yield n
        n += 1

البته، استفاده از چنین عباراتی بدون منطق خروج از حلقه توصیه نمی‌شود.

نکته

یکی از معایب تکرار با ژنراتور این است که فقط می‌توانید یک بار از ابتدا تا انتها روی عناصر تکرار کنید. اگر می‌خواهید چندین بار تکرار کنید، باید هر بار یک ژنراتور جدید ایجاد کنید یا از لیست استفاده کنید.

روش دوم برای ایجاد ژنراتور: استفاده از عبارات کامپریهنشن درون پرانتز:

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 تا 1 قرار دارند
# 0.420571580830845,          # این تابع پرکاربردترین تابع برای تولید اعداد تصادفی است
# 0.25891675029296335]

اگر می‌خواهید نتایج قابل تکرار داشته باشید، می‌توانید ماژول random را وادار کنید تا اعداد شبه‌تصادفی (یعنی قطعی) را بر اساس وضعیت داخلی تنظیم شده توسط random.seed تولید کند:

random.seed(10)           # seed را روی 10 تنظیم می‌کند
print random.random()     # 0.57140259469
random.seed(10)           # seed را دوباره روی 10 تنظیم می‌کند
print random.random()     # 0.57140259469 دوباره

گاهی اوقات نیز از تابع 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) # [16, 36, 10, 6, 25, 9]

می‌توانید با چندین بار فراخوانی، نمونه‌های تصادفی متعددی را انتخاب کنید (با تکرار مجاز):

four_with_replacement = [random.choice(range(10))
                         for _ in range(4)]
# [9, 4, 4, 2]

عبارات با قاعده 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

مانند بسیاری از زبان‌ها، پایتون به شما امکان می‌دهد کلاس‌هایی برای کپسوله‌سازی داده‌ها و توابعی برای کار با آن‌ها تعریف کنید. ما گاهی اوقات از آن‌ها استفاده می‌کنیم تا کدمان واضح‌تر و مختصرتر شود. شاید ساده‌ترین راه برای توضیح آن‌ها، ساخت یک مثال با توضیحات فراوان باشد. فرض کنید پایتون مجموعه (set) داخلی نداشت؛ در این صورت ممکن بود بخواهیم کلاس Set خودمان را ایجاد کنیم. خب، این کلاس باید چه قابلیت‌هایی داشته باشد؟ مثلاً، با داشتن یک Set، باید بتوانیم آیتم‌ها را به آن اضافه کنیم، از آن حذف کنیم و بررسی کنیم که آیا مقدار خاصی را شامل می‌شود یا خیر. بنابراین، تمام این قابلیت‌ها را به عنوان توابع عضو (member functions) این کلاس ایجاد خواهیم کرد. به این ترتیب، می‌توانیم پس از شیء Set، با استفاده از نقطه به این توابع عضو دسترسی پیدا کنیم:

# طبق قرارداد، نام کلاس را با _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

ما می‌خواهیم با استفاده از آن، تابعی ایجاد کنیم که یک متغیر را به عنوان ورودی بگیرد و نتیجه تابع توان با پایه 2، یعنی 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)               # 8

اگر نامی مشخص شود، می‌توان از متد partial برای پر کردن پارامترهای دیگر نیز استفاده کرد:

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

اگر سعی کنید پارامترها را به صورت نامنظم در میانه تابع دستکاری کنید، برنامه به سرعت گیج‌کننده خواهد شد؛ بنابراین لطفاً از این کار خودداری کنید.

نگاشت 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])  # [1 * 4, 2 * 5] = [4, 10]

فیلتر 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)          # = 1 * 2 * 3 * 4 = 24
list_product = partial(reduce, multiply)  # این تابع یک لیست را کاهش می‌دهد
x_product = list_product(xs)              # نیز 24 است

شمارش 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 است که یک تاپل (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

اگر طول لیست‌های متعدد یکسان نباشد، فرآیند فشرده‌سازی در انتهای کوتاه‌ترین لیست متوقف می‌شود. همچنین می‌توانید از یک ترفند جالب برای بازگشایی (unzip) لیست‌ها استفاده کنید:

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

در اینجا، ستاره * برای انجام بازگشایی آرگومان‌ها استفاده می‌شود؛ این کار عناصر 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)           # برمی‌گرداند 3
add([1, 2])         # خطا می‌دهد
add(*[1, 2])        # برمی‌گرداند 3

اگرچه ممکن است خیلی کاربردی نباشد، اما یک ترفند خوب برای مختصرتر کردن کد است.

ارسال آرگومان‌های با طول نامشخص args and kwargs

فرض کنید می‌خواهیم یک تابع مرتبه بالا (higher-order function) ایجاد کنیم که یک تابع قدیمی را به عنوان ورودی می‌گیرد و یک تابع جدید برمی‌گرداند که نتیجه تابع قدیمی را در 2 ضرب می‌کند:

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

مثال اجرا:

def f1(x):
    return x + 1

g = doubler(f1)
print g(3)        # 8 (== ( 3 + 1) * 2)
print g(-1)       # 0 (== (-1 + 1) * 2)

اما اگر تعداد پارامترهای ارسال شده بیش از یکی باشد، این روش دیگر کارایی ندارد:

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)    # 6

می‌توانید از آن با روش‌های عجیب و غریب مختلفی استفاده کنید، اما ما فقط از آن برای حل مشکل ارسال آرگومان‌های با طول نامشخص به توابع مرتبه بالا استفاده می‌کنیم:

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) # 6

به دنیای علم داده خوش آمدید!

دینگ! به شما تبریک می‌گویم که دروازه دنیایی جدید را باز کردید! حالا می‌توانید با خوشحالی به کاوش بپردازید~

مطالعه بیشتر:

سینتکس پرکاربرد پایتون در علم داده (مقدماتی)