آموزش پایتون از صفر – بخش چهارم: محدوده متغیرها، دیباگ کردن و ساخت بازی حدس عدد

بازدید: 58 بازدید
آموزش پایتون از صفر بخش چهارم؛ محدوده متغیرها و دیباگ کردن

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

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

این آموزش با الهام از دوره Python Bootcamp از Angela Yu و با بازنویسی آموزشی، مثال‌های تکمیلی و تمرین‌های اختصاصی تهیه شده است.

فهرست مطالب این بخش

1. محدوده متغیرها در پایتون

هر متغیری که در پایتون ایجاد می کنیم، در بخش مشخصی از برنامه قابل دسترسی است. به این محدوده، Scope گفته می شود.

برای مثال:

enemies = 3


def show_enemies():
    enemies = 5
    print(f"Enemies inside function: {enemies}")


show_enemies()

print(f"Enemies outside function: {enemies}")

خروجی:

Enemies inside function: 5
Enemies outside function: 3

با اینکه هر دو متغیر نام یکسانی دارند، یکی داخل تابع و دیگری خارج از تابع قرار گرفته است. به همین دلیل پایتون آن ها را دو متغیر جداگانه در نظر می گیرد.

محدوده محلی

متغیری که داخل یک تابع تعریف می شود، معمولا فقط در همان تابع قابل استفاده است.

def calculate_price():
    discount = 20
    print(discount)


calculate_price()

متغیر discount فقط داخل تابع calculate_price وجود دارد.

اگر بخواهیم خارج از تابع به آن دسترسی داشته باشیم:

def calculate_price():
    discount = 20


calculate_price()

print(discount)

برنامه خطا می دهد، زیرا discount خارج از تابع شناخته شده نیست.

محدوده سراسری

اگر متغیری خارج از تمام توابع تعریف شود، در محدوده سراسری قرار می گیرد.

username = "Ali"


def welcome():
    print(f"Welcome {username}")


welcome()

تابع می تواند مقدار username را بخواند، چون این متغیر خارج از تابع تعریف شده است.

خروجی:

Welcome Ali

2. فضای نام در پایتون

پایتون برای مدیریت نام متغیرها، توابع و سایر اجزای برنامه از مفهوم Namespace یا فضای نام استفاده می کند. هر محدوده می تواند فضای نام مخصوص خودش را داشته باشد.

message = "Outside function"


def show_message():
    message = "Inside function"
    print(message)


show_message()

print(message)

خروجی:

Inside function
Outside function

در این مثال دو متغیر با نام message داریم، اما هر کدام در فضای نام متفاوتی قرار گرفته اند.

متغیر داخل تابع متعلق به فضای نام محلی تابع است و متغیر بیرون تابع در فضای نام سراسری قرار دارد.

3. نبود محدوده بلوکی در پایتون

در پایتون ساختارهایی مانند if و for معمولا محدوده محلی جدید ایجاد نمی کنند.

برای مثال:

game_level = 3

enemies = ["Skeleton", "Zombie", "Alien"]

if game_level < 5:
    new_enemy = enemies[0]

print(new_enemy)

خروجی:

Skeleton

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

for number in range(3):
    result = number * 2

print(result)

خروجی:

4

اما تابع محدوده مخصوص خودش را ایجاد می کند:

def create_enemy():
    enemy = "Zombie"


create_enemy()

print(enemy)

این برنامه خطا می دهد، چون متغیر enemy فقط داخل تابع وجود دارد.

پس باید به خاطر داشته باشیم که شرط ها و حلقه ها محدوده محلی مستقل ایجاد نمی کنند، اما توابع این کار را انجام می دهند.

4. تغییر متغیر سراسری داخل تابع

فرض کنید یک متغیر سراسری برای تعداد دشمنان بازی داریم:

enemies = 1

اگر بخواهیم مستقیما داخل تابع مقدار آن را تغییر دهیم:

enemies = 1


def increase_enemies():
    enemies += 1


increase_enemies()

برنامه خطا می دهد.

پایتون تصور می کند enemies داخل تابع یک متغیر محلی است، اما قبل از افزایش مقدار، هیچ مقدار اولیه ای برای آن تعریف نشده است.

استفاده از global

با کلمه کلیدی global می توانیم مشخص کنیم که منظور ما همان متغیر سراسری است.

enemies = 1


def increase_enemies():
    global enemies

    enemies += 1

    print(f"Enemies inside function: {enemies}")


increase_enemies()

print(f"Enemies outside function: {enemies}")

خروجی:

Enemies inside function: 2
Enemies outside function: 2

این روش کار می کند، اما بهتر است تا جای ممکن متغیرهای سراسری را مستقیما از داخل توابع تغییر ندهیم.

روش بهتر برای تغییر مقدار

روش بهتر این است که مقدار را به تابع بدهیم و مقدار جدید را با return برگردانیم.

enemies = 1


def increase_enemies(current_enemies):
    return current_enemies + 1


enemies = increase_enemies(enemies)

print(enemies)

خروجی:

2

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

5. ثابت های سراسری

گاهی در برنامه مقادیری داریم که قرار نیست تغییر کنند.

برای مثال:

PI = 3.14159
#یا
MAX_ATTEMPTS = 5

در پایتون چیزی مانع تغییر این متغیرها نمی شود، اما طبق یک قرارداد رایج، نام ثابت ها را با حروف بزرگ می نویسیم.

DEFAULT_SPEED = 100
MAX_PLAYERS = 4
TAX_RATE = 0.10

این نوع نام گذاری به سایر برنامه نویسان نشان می دهد که این مقادیر قرار نیست در طول اجرای برنامه تغییر کنند.

6. پروژه بازی حدس عدد

در این پروژه کامپیوتر یک عدد تصادفی بین 1 تا 100 انتخاب می کند و کاربر باید آن را پیدا کند.

بازی دارای دو سطح است:

  • سطح آسان با 10 فرصت
  • سطح سخت با 5 فرصت

بعد از هر حدس نیز برنامه مشخص می کند که عدد وارد شده بزرگ تر یا کوچک تر از جواب صحیح است.

تولید عدد تصادفی

برای تولید عدد تصادفی از randint استفاده می کنیم.

from random import randint

حالا می توانیم یک عدد بین 1 تا 100 تولید کنیم:

answer = randint(1, 100)

هر بار که برنامه اجرا شود، مقدار متفاوتی ممکن است در answer قرار بگیرد.

تعریف تعداد فرصت ها

تعداد فرصت های هر سطح را به صورت ثابت تعریف می کنیم:

EASY_LEVEL_TURNS = 10
HARD_LEVEL_TURNS = 5

استفاده از ثابت ها باعث می شود تنظیمات اصلی بازی در یک قسمت مشخص قرار بگیرند.

ساخت تابع بررسی پاسخ

حالا تابعی می سازیم که حدس کاربر را با عدد اصلی مقایسه کند.

def check_answer(guess, answer, turns):
    if guess > answer:
        print("Too high.")
        return turns - 1

    elif guess < answer:
        print("Too low.")
        return turns - 1

    else:
        print(f"You got it! The answer was {answer}.")
        return turns

این تابع سه ورودی دریافت می کند:

  • guess حدس کاربر
  • answer عدد صحیح
  • turns تعداد فرصت های باقی مانده

اگر حدس اشتباه باشد، یک فرصت کم می شود و مقدار جدید برگردانده می شود.

ساخت تابع انتخاب سطح بازی

حالا از کاربر می خواهیم سطح سختی را انتخاب کند.

def set_difficulty():
    level = input("Choose a difficulty. Type 'easy' or 'hard': ").lower()

    if level == "easy":
        return EASY_LEVEL_TURNS
    else:
        return HARD_LEVEL_TURNS

اگر کاربر easy را انتخاب کند 10 فرصت دارد و در غیر این صورت 5 فرصت دریافت می کند.

ساخت تابع اصلی بازی

بدنه اصلی بازی را داخل تابع game قرار می دهیم.

def game():
    print("Welcome to the number guessing game!")
    print("I'm thinking of a number between 1 and 100.")

    answer = randint(1, 100)

    turns = set_difficulty()

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

دریافت حدس در حلقه

تا زمانی که کاربر عدد درست را پیدا نکرده باشد، باید بازی ادامه پیدا کند.

guess = 0

while guess != answer:
    print(f"You have {turns} attempts remaining.")

    guess = int(input("Make a guess: "))

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

کم کردن فرصت ها

بعد از دریافت حدس، تابع check_answer را اجرا می کنیم:

turns = check_answer(guess, answer, turns)

اگر کاربر اشتباه حدس زده باشد، تابع یک فرصت از او کم می کند.

بررسی پایان بازی

اگر فرصت ها تمام شوند، باید بازی متوقف شود.

if turns == 0:
    print(f"You've run out of guesses. The answer was {answer}.")
    return

return اجرای تابع game را متوقف می کند.

اگر هنوز فرصت باقی مانده باشد اما جواب اشتباه باشد:

elif guess != answer:
    print("Guess again.")

کاربر دوباره وارد حلقه می شود و حدس بعدی را وارد می کند.

کد کامل بازی حدس عدد

from random import randint


EASY_LEVEL_TURNS = 10
HARD_LEVEL_TURNS = 5


def check_answer(guess, answer, turns):
    if guess > answer:
        print("Too high.")
        return turns - 1

    elif guess < answer:
        print("Too low.")
        return turns - 1

    else:
        print(f"You got it! The answer was {answer}.")
        return turns


def set_difficulty():
    level = input("Choose a difficulty. Type 'easy' or 'hard': ").lower()

    if level == "easy":
        return EASY_LEVEL_TURNS
    else:
        return HARD_LEVEL_TURNS


def game():
    print("Welcome to the number guessing game!")
    print("I'm thinking of a number between 1 and 100.")

    answer = randint(1, 100)

    turns = set_difficulty()
    guess = 0

    while guess != answer:
        print(f"You have {turns} attempts remaining.")

        guess = int(input("Make a guess: "))

        turns = check_answer(guess, answer, turns)

        if turns == 0:
            print(f"You've run out of guesses. The answer was {answer}.")
            return

        elif guess != answer:
            print("Guess again.")


game()

در این پروژه از توابع، شرط ها، حلقه while، مقدار بازگشتی، تولید عدد تصادفی، ثابت های سراسری و محدوده متغیرها در کنار یکدیگر استفاده کردیم.

7. دیباگ کردن در پایتون

دیباگ کردن یعنی پیدا کردن و اصلاح مشکلات برنامه. بعضی خطاها باعث توقف کامل برنامه می شوند و بعضی دیگر بدون نمایش پیام خطا، نتیجه اشتباهی تولید می کنند.

یکی از بهترین روش ها برای یادگیری دیباگ این است که چند خطا را همزمان در یک برنامه بررسی کنیم.

مثال: پیدا کردن چند خطا در یک برنامه

فرض کنید می خواهیم برنامه ای بنویسیم که تعدادی نمره از یک لیست را بررسی کند، نمره های قبولی را دو برابر کند و داخل لیست جدید قرار دهد.

کد اولیه به شکل زیر نوشته شده است:

scores = ["12", "18", "7", "20"]

passed_scores = []

for index in range(1, 5):
    score = scores[index]

    if score >= 10:
        new_score = score * 2

passed_scores.append(new_score)

print(passed_scores)

این برنامه چند مشکل مختلف دارد.

اولین مشکل این است که اعضای لیست به صورت رشته ذخیره شده اند:

scores = ["12", "18", "7", "20"]

در نتیجه نمی توانیم مستقیما مقدار score را با عدد 10 مقایسه کنیم.

مشکل دوم در محدوده range قرار دارد:

range(1, 5)

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

مشکل سوم مربوط به تورفتگی است. دستور:

passed_scores.append(new_score)

خارج از حلقه قرار گرفته است و در نتیجه برای هر نمره اجرا نمی شود.

مشکل دیگر این است که اگر بخواهیم مقدار عددی را دو برابر کنیم، ابتدا باید رشته را به عدد تبدیل کنیم.

نسخه اصلاح شده برنامه:

scores = ["12", "18", "7", "20"]

passed_scores = []

for index in range(0, 4):
    score = int(scores[index])

    if score >= 10:
        new_score = score * 2
        passed_scores.append(new_score)

print(passed_scores)

خروجی:

[24, 36, 40]

این مثال چند نکته مهم دیباگ را به ما یاد می دهد:

  • همیشه نوع داده متغیرها را بررسی کنید.
  • به اندیس های لیست توجه داشته باشید.
  • مقدار ابتدایی و انتهایی range را بررسی کنید.
  • تورفتگی خطوط را با دقت ببینید.
  • بررسی کنید هر دستور دقیقا در کدام قسمت برنامه اجرا می شود.

استفاده از print برای پیدا کردن خطا

اگر برنامه اجرا می شود اما نتیجه مورد انتظار را نمی دهد، می توانیم مقادیر میانی را چاپ کنیم.

برای مثال:

scores = ["12", "18", "7", "20"]

passed_scores = []

for index in range(0, 4):
    score = int(scores[index])

    print(f"Current index: {index}")
    print(f"Current score: {score}")

    if score >= 10:
        new_score = score * 2

        print(f"New score: {new_score}")

        passed_scores.append(new_score)

print(passed_scores)

با این روش می توانیم مرحله به مرحله ببینیم برنامه چه مقادیری تولید می کند.

بررسی مقادیر مرزی

یکی دیگر از خطاهای رایج مربوط به شرایط مرزی است.

مثلا:

age = int(input("Enter your age: "))

if age > 18:
    print("Adult")
elif age < 18:
    print("Under 18")

اگر کاربر عدد 18 را وارد کند، هیچ خروجی نمایش داده نمی شود.

نسخه درست:

age = int(input("Enter your age: "))

if age >= 18:
    print("Adult")
else:
    print("Under 18")

هنگام دیباگ کردن شرط ها باید همیشه مقادیر مرزی مانند 0، 10، 18، 100 یا هر عددی که مرز بین دو شرط است را آزمایش کنیم.

استفاده از دیباگر

برای برنامه های کوچک استفاده از print بسیار مفید است، اما در برنامه های بزرگ تر بهتر است از Debugger استفاده کنیم.

برای مثال:

def calculate_total(numbers):
    total = 0

    for number in numbers:
        total += number

    return total


result = calculate_total([10, 20, 30, 40])

print(result)

می توانیم روی این خط یک Breakpoint قرار دهیم:

total += number

سپس برنامه را مرحله به مرحله اجرا کنیم و مقدار number و total را در هر مرحله ببینیم. این روش زمانی بسیار مفید است که برنامه اجرا می شود اما نمی دانیم دقیقا در کدام مرحله مقدار اشتباهی تولید شده است.

8. پروژه بازی مقایسه محبوبیت

در پروژه دوم، دو فرد به بازیکن نمایش داده می شوند و کاربر باید حدس بزند کدام فرد دنبال کننده بیشتری دارد.

اطلاعات هر فرد را در قالب یک دیکشنری ذخیره می کنیم و همه دیکشنری ها را داخل یک لیست قرار می دهیم.

ساخت داده های بازی

game_data = [
    {
        "name": "Alex Morgan",
        "follower_count": 180,
        "description": "Athlete",
        "country": "United States"
    },
    {
        "name": "Lucas Martin",
        "follower_count": 245,
        "description": "Actor",
        "country": "France"
    },
    {
        "name": "Emma Lee",
        "follower_count": 310,
        "description": "Singer",
        "country": "Canada"
    },
    {
        "name": "Daniel Costa",
        "follower_count": 205,
        "description": "Content creator",
        "country": "Brazil"
    },
    {
        "name": "Sofia Adams",
        "follower_count": 275,
        "description": "Model",
        "country": "United Kingdom"
    }
]

آماده کردن اطلاعات برای نمایش

تعداد دنبال کننده ها نباید به بازیکن نمایش داده شود. بنابراین تابعی می سازیم که فقط اطلاعات مورد نیاز را برگرداند.

def format_data(person):
    name = person["name"]
    description = person["description"]
    country = person["country"]

    return f"{name}, a {description}, from {country}"

بررسی پاسخ کاربر

def check_answer(guess, a_followers, b_followers):
    if a_followers > b_followers:
        return guess == "a"
    else:
        return guess == "b"

اگر تعداد دنبال کننده های گزینه A بیشتر باشد، پاسخ کاربر باید a باشد. در غیر این صورت پاسخ صحیح b خواهد بود.

کد کامل بازی

import random


game_data = [
    {
        "name": "Alex Morgan",
        "follower_count": 180,
        "description": "Athlete",
        "country": "United States"
    },
    {
        "name": "Lucas Martin",
        "follower_count": 245,
        "description": "Actor",
        "country": "France"
    },
    {
        "name": "Emma Lee",
        "follower_count": 310,
        "description": "Singer",
        "country": "Canada"
    },
    {
        "name": "Daniel Costa",
        "follower_count": 205,
        "description": "Content creator",
        "country": "Brazil"
    },
    {
        "name": "Sofia Adams",
        "follower_count": 275,
        "description": "Model",
        "country": "United Kingdom"
    }
]


def format_data(person):
    name = person["name"]
    description = person["description"]
    country = person["country"]

    return f"{name}, a {description}, from {country}"


def check_answer(guess, a_followers, b_followers):
    if a_followers > b_followers:
        return guess == "a"
    else:
        return guess == "b"


score = 0
game_should_continue = True

person_b = random.choice(game_data)


while game_should_continue:
    person_a = person_b
    person_b = random.choice(game_data)

    while person_a == person_b:
        person_b = random.choice(game_data)

    print()
    print("Compare A:")
    print(format_data(person_a))

    print()
    print("VS")
    print()

    print("Against B:")
    print(format_data(person_b))

    guess = input("Who has more followers? Type 'A' or 'B': ").lower()

    a_followers = person_a["follower_count"]
    b_followers = person_b["follower_count"]

    is_correct = check_answer(
        guess,
        a_followers,
        b_followers
    )

    if is_correct:
        score += 1
        print(f"You're right! Current score: {score}")
    else:
        game_should_continue = False
        print(f"Sorry, that's wrong. Final score: {score}")

یکی از نکات مهم بازی این است که گزینه B مرحله قبل در دور بعد به گزینه A تبدیل می شود:

person_a = person_b

سپس یک فرد جدید برای گزینه B انتخاب می کنیم:

person_b = random.choice(game_data)

برای اینکه هر دو گزینه یک فرد نباشند نیز این بررسی انجام می شود:

while person_a == person_b:
    person_b = random.choice(game_data)

به این ترتیب بازی تا زمانی که کاربر پاسخ صحیح بدهد ادامه پیدا می کند.

جمع‌بندی بخش چهارم

در این بخش با محدوده محلی و سراسری متغیرها، فضای نام و نبود محدوده بلوکی در پایتون آشنا شدیم. همچنین یاد گرفتیم که بهتر است تا جای ممکن به جای تغییر مستقیم متغیرهای سراسری، مقدار جدید را از تابع برگردانیم.

بعد از آن بازی حدس عدد را مرحله به مرحله ساختیم و دیدیم چگونه می توان توابع، حلقه ها، شرط ها، ثابت ها و تولید اعداد تصادفی را در یک پروژه واقعی کنار هم قرار داد.

در بخش دیباگ نیز یاد گرفتیم که بررسی نوع داده، اندیس لیست، محدوده range، تورفتگی، شرایط مرزی و چاپ مقادیر میانی می تواند به پیدا کردن بسیاری از خطاهای برنامه کمک کند.


9. چند تمرین برای این بخش:

تمرین 1: تابعی بنویسید که قیمت یک محصول را دریافت کند و 15 درصد تخفیف را از آن کم کند. قیمت نهایی باید با return برگردانده شود

مشاهده پاسخ تمرین 1

def calculate_discount(price):
    discount = price * 0.15
    final_price = price - discount

    return final_price


result = calculate_discount(800)

print(result)

تمرین 2: متغیری به نام score با مقدار 20 بسازید. سپس تابعی بنویسید که 10 امتیاز به مقدار فعلی اضافه کند، بدون اینکه از global استفاده شود.

مشاهده پاسخ تمرین 2

score = 20


def increase_score(current_score):
    return current_score + 10


score = increase_score(score)

print(score)

تمرین 3: کد زیر قرار است تمام اعداد لیست را سه برابر کند، اما نتیجه درستی تولید نمی کند. مشکل را پیدا و اصلاح کنید.

def triple_numbers(numbers):
    result = []

    for number in numbers:
        new_number = number * 3

    result.append(new_number)

    return result
مشاهده پاسخ تمرین 3

def triple_numbers(numbers):
    result = []

    for number in numbers:
        new_number = number * 3
        result.append(new_number)

    return result


print(triple_numbers([1, 2, 3, 4]))

تمرین 4: برنامه ای بنویسید که یک عدد از کاربر دریافت کند. اگر عدد کمتر از 10 بود Low، اگر بین 10 تا 20 بود Medium و اگر بیشتر از 20 بود High نمایش داده شود.

مشاهده پاسخ تمرین 4

number = int(input("Enter a number: "))


if number < 10:
    print("Low")

elif number <= 20:
    print("Medium")

else:
    print("High")

تمرین 5: یک عدد تصادفی بین 1 تا 20 انتخاب کنید. کاربر فقط 4 فرصت داشته باشد تا آن را حدس بزند. اگر حدس بزرگ تر از جواب بود Too high و اگر کوچک تر بود Too low نمایش داده شود. در صورت تمام شدن فرصت ها نیز جواب صحیح چاپ شود.

مشاهده پاسخ تمرین 5

from random import randint


MAX_ATTEMPTS = 4


def check_guess(guess, answer):
    if guess > answer:
        print("Too high.")
        return False

    elif guess < answer:
        print("Too low.")
        return False

    else:
        print("Correct!")
        return True


answer = randint(1, 20)

attempts = MAX_ATTEMPTS
game_over = False


while attempts > 0 and not game_over:
    print(f"You have {attempts} attempts remaining.")

    guess = int(input("Guess a number between 1 and 20: "))

    game_over = check_guess(guess, answer)

    if not game_over:
        attempts -= 1


if not game_over:
    print(f"You lost. The correct answer was {answer}.")

ادامه مطلب